Linked List Operations in C Read Proof
1. You can add some comments !

2. In my opinion you should create another struct that will be named like linked_list_t which will have two pointers, node_t * start ,node_t* end.In terms of efficiency, by doing this, you don't have to iterate all the list on add_at_end to add a single node (this is a common practice when you create a linked_list)

3. When you malloc you can check the returned pointer and if it is NULL you should perror() the message.

4. The code 

typedef struct node {
    int data;
    struct node * next;
} node_t;

void print_list(node_t * head);
void push(node_t ** head, int data);
int pop(node_t ** head);
int add_at_end(node_t ** head, int data);
int remove_last(node_t * head);
int add_at_index(node_t ** head, int n, int data);
int remove_by_index(node_t ** head, int n);


should be put on a header file as it just contains declarations 

5. Use Sentinel Nodes in order not to iterate on int remove_last(node_t *)