Repository: kuzmoyev/google-calendar-simple-api (language: python)

Issue:
Add calendars methods
https://developers.google.com/calendar/v3/reference/calendarList
https://developers.google.com/calendar/v3/reference/calendars


Discussion / hints:
Let me know if I can help implementing this. Would be really helpful to have these in gcsa.

Also, I am confused at how I can access different calendars for the same person using gcsa?
@joshsny
For different user calendars, you can specify `GoogleCalendar(calendar=<calendar_id>)` parameter. See more in #7 (I really need to update the docs to make it more clear).

As for the calendar methods, I'd be very grateful for a PR, but it is a quite complicated piece of work and I'm not sure how to integrate it yet.

If you'd like to work on it, it would be better to decide on it here before the implementation. For instance, what object will be responsible for these methods (`GoogleCalendar` itself or other, something like `GoogleCalendarServise`. I'm thinking the latter)?


Also, as a workaround, you can use the `gc.service` to perform all the calendar/calendarList methods.
Ah I must have not read the docs correctly, didn't see that I can use `gc.service` , that's very useful.

I worked on something similar and chose a different object like you suggest, then used `GoogleCalendarService` to return a list of `GoogleCalendar` objects. I'd contribute it here but the code is a complete mess.

Thanks for all the work you've put into this, it's been very useful!
I guess you're not supposed to use `gc.service` - that's the whole point of the library :) It's just a workaround, so it's not documented

Fix patch:
diff --git a/.github/workflows/code-cov.yml b/.github/workflows/code-cov.yml
index 8b3f1b3b..612a149e 100644
--- a/.github/workflows/code-cov.yml
+++ b/.github/workflows/code-cov.yml
@@ -13,7 +13,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v2
         with:
-          python-version: 3.9
+          python-version: '3.10'
 
       - name: Install dependencies
         run: pip install tox
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 1fd93011..960c1e8a 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -1,3 +1,16 @@
+version: 2
+
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.10"
+
+sphinx:
+  configuration: docs/source/conf.py
+
 python:
-   version: 3.5
-   setup_py_install: true
\ No newline at end of file
+  install:
+    - method: pip
+      path: .
+      extra_requirements:
+        - docs
diff --git a/README.rst b/README.rst
index 29a32758..93560c79 100644
--- a/README.rst
+++ b/README.rst
@@ -18,7 +18,7 @@ Google Calendar Simple API
     :alt: Discord
 
 
-`Google Calendar Simple API` or `gcsa` is a library that simplifies event management in Google Calendars.
+`Google Calendar Simple API` or `gcsa` is a library that simplifies event and calendar management in Google Calendars.
 It is a Pythonic object oriented adapter for the official API. See the full `documentation`_.
 
 Installation
@@ -88,5 +88,5 @@ Template for `setup.py` was taken from `kennethreitz/setup.py`_
 
 .. _documentation: https://google-calendar-simple-api.readthedocs.io/en/latest/?badge=latest
 .. _`Getting started page`: https://google-calendar-simple-api.readthedocs.io/en/latest/getting_started.html
-.. _beautiful_date: https://github.com/beautiful-everything/beautiful-date
+.. _beautiful_date: https://github.com/kuzmoyev/beautiful-date
 .. _`kennethreitz/setup.py`: https://github.com/kennethreitz/setup.py
diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css
index 83d41d4e..4a35cdcb 100644
--- a/docs/source/_static/css/custom.css
+++ b/docs/source/_static/css/custom.css
@@ -1,16 +1,16 @@
-/*Newlines (\a) and spaces (\20) before each parameter*/
-dl.class em:not([class])::before {
+/* Newlines (\a) and spaces (\20) before each parameter */
+.sig-param::before {
     content: "\a\20\20\20\20\20\20\20\20\20\20\20\20\20\20\20\20";
     white-space: pre;
 }
 
-/*Newline after the last parameter (so the closing bracket is on a new line)*/
-dl.class em:not([class]):last-of-type::after {
+/* Newline after the last parameter (so the closing bracket is on a new line) */
+dt em.sig-param:last-of-type::after {
     content: "\a";
     white-space: pre;
 }
 
-/*To have blue background of width of the block (instead of width of content)*/
+/* To have blue background of width of the block (instead of width of content) */
 dl.class > dt:first-of-type {
     display: block !important;
 }
\ No newline at end of file
diff --git a/docs/source/_templates/footer.html b/docs/source/_templates/footer.html
new file mode 100644
index 00000000..03c9f9f8
--- /dev/null
+++ b/docs/source/_templates/footer.html
@@ -0,0 +1,10 @@
+{% extends "!footer.html" %}
+{% block extrafooter %}{{ super() }}
+<div>
+    <br>
+    Portions of this page are reproduced from and/or are modifications based on work created and
+    <a href="https://developers.google.com/calendar/api/v3/reference">shared by Google</a>
+    and used according to terms described in the
+    <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons 4.0 Attribution License</a>.
+</div>
+{% endblock %}
diff --git a/docs/source/calendars.rst b/docs/source/calendars.rst
new file mode 100644
index 00000000..97764600
--- /dev/null
+++ b/docs/source/calendars.rst
@@ -0,0 +1,229 @@
+.. _calendars:
+
+Calendars and Calendar list
+============================
+
+Calendars in `gcsa` are represented by the :py:class:`~gcsa.calendar.Calendar` and
+:py:class:`~gcsa.calendar.CalendarListEntry` classes.
+
+
+Calendars vs Calendar List
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The **Calendars** collection represents all existing calendars. It can be used to create and delete calendars. You can also
+retrieve or set global properties shared across all users with access to a calendar. For example, a calendar's title and
+default time zone are global properties.
+
+The **Calendar List** is a collection of all calendar entries that a user has added to their list (shown in the left panel
+of the web UI). You can use it to add and remove existing calendars to/from the users’ list. You also use it to retrieve
+and set the values of user-specific calendar properties, such as default reminders. Another example is foreground color,
+since different users can have different colors set for the same calendar.
+
+The **GoogleCalendar** is a service responsible for processing API requests.
+
+Calendars
+~~~~~~~~~
+
+`gcsa` allows you to create a new calendar, retrieve, update, delete and clear existing calendars.
+
+To do so, create a :py:class:`~gcsa.google_calendar.GoogleCalendar` instance (see :ref:`getting_started` to get your
+credentials):
+
+.. code-block:: python
+
+    from gcsa.google_calendar import GoogleCalendar
+
+    gc = GoogleCalendar()
+
+
+Get calendar by id
+------------------
+This returns an objects that stores metadata of the calendar (see more in :py:class:`~gcsa.calendar.Calendar`).
+
+Get a calendar specified as a default in `GoogleCalendar()`
+
+.. code-block:: python
+
+    calendar = gc.get_calendar()
+
+To get a calendar other than the one specified as a default in `GoogleCalendar()`
+
+.. code-block:: python
+
+    calendar = gc.get_calendar('<calendar_id>')
+
+
+Add a secondary calendar
+------------------------
+
+.. code-block:: python
+
+    from gcsa.calendar import Calendar
+
+    calendar = Calendar(
+        'Travel calendar',
+        description = 'Calendar for travel related events'
+    )
+    calendar = gc.add_calendar(calendar)
+
+
+Update calendar
+---------------
+
+.. code-block:: python
+
+    calendar.summary = 'New summary'
+    calendar.description = 'New description'
+    calendar = gc.update_calendar(calendar)
+
+
+Delete calendar
+---------------
+
+.. code-block:: python
+
+    gc.delete_calendar(calendar)
+
+or by id
+
+.. code-block:: python
+
+    gc.delete_calendar('<calendar_id>')
+
+
+Calendar has to have ``calendar_id`` to be updated or deleted. Calendars that you get from
+:py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list` method already have their ids.
+You can also delete the calendar by providing its id.
+
+
+Clear calendar
+--------------
+
+You can only clear (remove all events from) **primary** calendar.
+
+.. code-block:: python
+
+    gc.clear_calendar()
+
+.. warning::
+    This will always try to clear a **primary** calendar, regardless of the `default_calendar` value.
+
+
+Calendar List
+~~~~~~~~~~~~~
+
+`gcsa` allows you to add an existing calendar into the user's calendar list, retrieve user's calendar list,
+retrieve, update, and delete single entries in the user's calendar list.
+
+To do so, create a :py:class:`~gcsa.google_calendar.GoogleCalendar` instance (see :ref:`getting_started` to get your
+credentials):
+
+.. code-block:: python
+
+    from gcsa.google_calendar import GoogleCalendar
+
+    gc = GoogleCalendar()
+
+
+Get user's calendar list
+------------------------
+
+This returns the collection of calendars in the user's calendar list.
+(see more in :py:class:`~gcsa.calendar.CalendarListEntry`).
+
+.. code-block:: python
+
+    for calendar in gc.get_calendar_list():
+        print(calendar)
+
+you can include deleted and hidden entries and specify the minimal access role:
+
+.. code-block:: python
+
+    from gcsa.calendar import AccessRoles
+
+    gc.get_calendar_list(
+        min_access_role=AccessRoles.READER
+        show_deleted=True,
+        show_hidden=True
+    )
+
+Get calendar list entry by id
+-----------------------------
+
+Get a calendar specified as a default in `GoogleCalendar()`
+
+.. code-block:: python
+
+    gc.get_calendar_list_entry()
+
+
+To get calendar other then the one specified as a default in `GoogleCalendar()`
+
+
+.. code-block:: python
+
+    gc.get_calendar_list_entry('calendar_id')
+
+
+Add calendar list entry
+-----------------------
+
+This adds an existing calendar into the user's calendar list
+(see more in :py:class:`~gcsa.calendar.CalendarListEntry`):
+
+
+.. code-block:: python
+
+    from gcsa.calendar import CalendarListEntry
+
+    calendar_list_entry = CalendarListEntry(
+        calendar_id='calendar_id',
+        summary_override='Holidays in Czechia'
+    )
+    gc.add_calendar_list_entry(calendar_list_entry)
+
+You can make a :py:class:`~gcsa.calendar.CalendarListEntry` out of :py:class:`~gcsa.calendar.Calendar` with
+:py:meth:`~gcsa.calendar.Calendar.to_calendar_list_entry` method:
+
+
+.. code-block:: python
+
+    calendar = Calendar(
+        calendar_id='calendar_id',
+        summary='Státní svátky v ČR'
+    )
+    calendar_list_entry = calendar.to_calendar_list_entry(
+        summary_override='Holidays in Czechia'
+    )
+    gc.add_calendar_list_entry(calendar_list_entry)
+
+
+Update calendar list entry
+--------------------------
+
+.. code-block:: python
+
+    calendar_list_entry.summary_override = 'Holidays in Czechia 2022'
+    gc.update_calendar_list_entry(calendar_list_entry)
+
+Delete calendar list entry
+--------------------------
+
+You can use :py:class:`~gcsa.calendar.CalendarListEntry`, :py:class:`~gcsa.calendar.Calendar`, or calendar id:
+
+.. code-block:: python
+
+    gc.delete_calendar_list_entry(calendar)
+
+or
+
+.. code-block:: python
+
+    gc.delete_calendar_list_entry(calendar_list_entry)
+
+or
+
+.. code-block:: python
+
+    gc.delete_calendar_list_entry('<calendar_id>')
+
diff --git a/docs/source/change_log.rst b/docs/source/change_log.rst
new file mode 100644
index 00000000..7ec22c09
--- /dev/null
+++ b/docs/source/change_log.rst
@@ -0,0 +1,107 @@
+.. _change_log:
+
+Change log
+==========
+
+
+v2.0.0
+~~~~~~
+
+API
+---
+* Adds calendar and calendar list related methods
+* Adds settings related method
+* Adds colors related method
+* Adds support for python3.10
+
+Core
+----
+* Separates ``GoogleCalendar`` into authentication, events, calendars, calendar list, colors, and settings services
+* Uses newest documentation generation libraries
+
+Backward compatibility
+----------------------
+* Full compatibility
+
+
+v1.3.0
+~~~~~~
+* Adds deletion of event by its id in ``GoogleCalendar.delete_event()``
+
+Core
+----
+* None
+
+Backward compatibility
+----------------------
+* Full compatibility
+
+
+v1.2.1
+~~~~~~
+* Adds ``Event.id`` in serialized event
+* Fixes conference's entry point without ``entry_point_type``
+
+Core
+----
+* Switches to tox for testing
+
+Backward compatibility
+----------------------
+* Full compatibility
+
+
+v1.2.0
+~~~~~~
+* Adds ``GoogleCalendar.import_event()`` method
+
+Core
+----
+* None
+
+Backward compatibility
+----------------------
+* Full compatibility
+
+
+v1.1.0
+~~~~~~
+* Fixes event creation without ``start`` and ``end``
+* Adds ``creator``, ``organizer`` and ``transparency`` fields to event
+
+Core
+----
+* None
+
+Backward compatibility
+----------------------
+* Full compatibility
+
+
+v1.0.1
+~~~~~~
+* Fixes ``GoogleCalendar.clear()`` method
+
+Core
+----
+* None
+
+Backward compatibility
+----------------------
+* Full compatibility
+
+
+v1.0.0 and previous versions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* Adds authentication management
+* Adds event management
+* Adds documentation in readthedocs.com
+
+Core
+----
+* Adds serializers for events and related objects
+* Adds automated testing in GitHub actions with code-coverage
+
+Backward compatibility
+----------------------
+* Full compatibility
\ No newline at end of file
diff --git a/docs/source/code/calendar.rst b/docs/source/code/calendar.rst
new file mode 100644
index 00000000..c0a949ef
--- /dev/null
+++ b/docs/source/code/calendar.rst
@@ -0,0 +1,20 @@
+Calendar
+========
+
+
+.. autoclass:: gcsa.calendar.Calendar
+    :members:
+    :undoc-members:
+
+
+.. autoclass:: gcsa.calendar.CalendarListEntry
+    :members:
+    :undoc-members:
+
+.. autoclass:: gcsa.calendar.NotificationType
+    :members:
+    :undoc-members:
+
+.. autoclass:: gcsa.calendar.AccessRoles
+    :members:
+    :undoc-members:
diff --git a/docs/source/code/code.rst b/docs/source/code/code.rst
index 66b60d93..bf0eef52 100644
--- a/docs/source/code/code.rst
+++ b/docs/source/code/code.rst
@@ -2,10 +2,11 @@ Code documentation
 ==================
 
 .. toctree::
-   :maxdepth: 2
+   :maxdepth: 3
    :caption: Contents:
 
    google_calendar
+   calendar
    event
    person
    attendees
@@ -13,3 +14,4 @@ Code documentation
    conference
    reminders
    recurrence
+   settings
diff --git a/docs/source/code/google_calendar.rst b/docs/source/code/google_calendar.rst
index 65d95d15..43881589 100644
--- a/docs/source/code/google_calendar.rst
+++ b/docs/source/code/google_calendar.rst
@@ -1,10 +1,32 @@
 GoogleCalendar
 ==============
 
-
 .. autoclass:: gcsa.google_calendar.GoogleCalendar
-    :members:
-    :undoc-members:
+        :members:
+            get_events,
+            get_instances,
+            get_event,
+            add_event,
+            add_quick_event,
+            update_event,
+            import_event,
+            move_event,
+            delete_event,
+            get_calendar,
+            add_calendar,
+            update_calendar,
+            delete_calendar,
+            clear_calendar,
+            clear,
+            get_calendar_list,
+            get_calendar_list_entry,
+            add_calendar_list_entry,
+            update_calendar_list_entry,
+            delete_calendar_list_entry,
+            list_event_colors,
+            list_calendar_colors,
+            get_settings
+        :undoc-members:
 
 .. autoclass:: gcsa.google_calendar.SendUpdatesMode
     :members:
diff --git a/docs/source/code/settings.rst b/docs/source/code/settings.rst
new file mode 100644
index 00000000..fe5b38b5
--- /dev/null
+++ b/docs/source/code/settings.rst
@@ -0,0 +1,7 @@
+Settings
+========
+
+
+.. autoclass:: gcsa.settings.Settings
+    :members:
+    :undoc-members:
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 1205e4aa..f15d076d 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -176,6 +176,7 @@
 autoclass_content = 'both'
 
 autodoc_member_order = 'bysource'
+autodoc_typehints = "description"
 
 html_theme_options = {
     'analytics_id': 'G-PT0TPQSZ56',
@@ -183,4 +184,4 @@
 
 html_css_files = [
     'css/custom.css',
-]
\ No newline at end of file
+]
diff --git a/docs/source/events.rst b/docs/source/events.rst
index b4afcef9..50a8ab02 100644
--- a/docs/source/events.rst
+++ b/docs/source/events.rst
@@ -6,7 +6,7 @@ Events
 Event in `gcsa` is represented by the class :py:class:`~gcsa.event.Event`. It stores all the needed information about
 the event including its summary, starting and ending dates/times, attachments, reminders, recurrence rules, etc.
 
-Current version of `gcsa` allows you to create a new events, retrieve, update, move and delete existing events.
+`gcsa` allows you to create a new events, retrieve, update, move and delete existing events.
 
 To do so, create a :py:class:`~gcsa.google_calendar.GoogleCalendar` instance (see :ref:`getting_started` to get your
 credentials):
@@ -15,7 +15,7 @@ credentials):
 
     from gcsa.google_calendar import GoogleCalendar
 
-    calendar = GoogleCalendar()
+    gc = GoogleCalendar()
 
 
 List events
@@ -25,7 +25,7 @@ This code will print out events for one year starting today:
 
 .. code-block:: python
 
-    for event in calendar:
+    for event in gc:
         print(event)
 
 
@@ -33,13 +33,13 @@ Specify range of listed events in two ways:
 
 .. code-block:: python
 
-    calendar.get_events(start_date, end_date, order_by='updated')
+    gc.get_events(start_date, end_date, order_by='updated')
 
 or
 
 .. code-block:: python
 
-    calendar[start_date:end_date:'updated']
+    gc[start_date:end_date:'updated']
 
 ``start_date`` and ``end_date`` can be ``date`` or ``datetime`` objects. ``order_by`` can be `'startTime'`
 or `'updated'`. If not specified, unspecified stable order is used.
@@ -49,15 +49,15 @@ Use ``query`` parameter for free text search through all event fields (except fo
 
 .. code-block:: python
 
-    calendar.get_events(query='Meeting')
-    calendar.get_events(query='John') # Name of attendee
+    gc.get_events(query='Meeting')
+    gc.get_events(query='John') # Name of attendee
 
 Use ``single_events`` parameter to expand recurring events into instances and only return single one-off events and
 instances of recurring events, but not the underlying recurring events themselves.
 
 .. code-block:: python
 
-    calendar.get_events(single_events=True)
+    gc.get_events(single_events=True)
 
 
 Get event by id
@@ -65,7 +65,7 @@ Get event by id
 
 .. code-block:: python
 
-    calendar.get_event('<event_id>')
+    gc.get_event('<event_id>')
 
 
 List recurring event instances
@@ -73,13 +73,13 @@ List recurring event instances
 
 .. code-block:: python
 
-    calendar.get_instances('<recurring_event_id>')
+    gc.get_instances('<recurring_event_id>')
 
 or
 
 .. code-block:: python
 
-    calendar.get_instances(recurring_event)
+    gc.get_instances(recurring_event)
 
 where ``recurring_event`` is :py:class:`~gcsa.event.Event` object with set ``event_id``. You'd probably get it from
 the ``get_events`` method.
@@ -99,7 +99,7 @@ Create event
                   start=start,
                   end=end)
 
-or to create an all-day event, use a `date` object:
+or to create an **all-day** event, use a `date` object:
 
 .. code-block:: python
 
@@ -119,7 +119,7 @@ Now **add** your event to the calendar:
 
 .. code-block:: python
 
-    calendar.add_event(event)
+    gc.add_event(event)
 
 See dedicated pages on how to add :ref:`attendees`, :ref:`attachments`, :ref:`conference`, :ref:`reminders`, and
 :ref:`recurrence` to an event.
@@ -131,7 +131,7 @@ Update event
 .. code-block:: python
 
     event.location = 'Prague'
-    calendar.update_event(event)
+    gc.update_event(event)
 
 
 Import event
@@ -139,7 +139,7 @@ Import event
 
 .. code-block:: python
 
-    calendar.import_event(event)
+    gc.import_event(event)
 
 This operation is used to add a private copy of an existing event to a calendar.
 
@@ -149,7 +149,7 @@ Move event to another calendar
 
 .. code-block:: python
 
-    calendar.move_event(event, destination_calendar_id='primary')
+    gc.move_event(event, destination_calendar_id='primary')
 
 
 Delete event
@@ -157,26 +157,17 @@ Delete event
 
 .. code-block:: python
 
-    calendar.delete_event(event)
+    gc.delete_event(event)
 
 
-Event has to have ``event_id`` to be updated, moved or deleted. Events that you get from
+Event has to have ``event_id`` to be updated, moved, or deleted. Events that you get from
 :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_events` method already have their ids.
 You can also delete the event by providing its id.
 
 .. code-block:: python
 
-    calendar.delete_event('<event_id>')
+    gc.delete_event('<event_id>')
 
 
-Clear calendar
-~~~~~~~~~~~~~~
-
-Remove all events from the calendar:
-
-.. code-block:: python
-
-    calendar.clear()
-
 .. _datetime: https://docs.python.org/3/library/datetime.html
 .. _beautiful_date: https://github.com/kuzmoyev/beautiful-date
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 7e073ddf..216c2cfc 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -1,7 +1,7 @@
-Welcome to Google Calendar Simple API's documentation!
+Welcome to Google Calendar Simple API documentation!
 ======================================================
 
-`Google Calendar Simple API` or `gcsa` is a library that simplifies event management in a Google Calendars.
+`Google Calendar Simple API` or `gcsa` is a library that simplifies event and calendar management in Google Calendars.
 It is a Pythonic object oriented adapter for the `official API`_.
 
 Example usage
@@ -59,13 +59,16 @@ Contents
    getting_started
    authentication
    events
+   calendars
    attendees
    attachments
    conference
    reminders
    recurrence
    serializers
+   settings
    why_gcsa
+   change_log
    code/code
 
 Indices and tables
diff --git a/docs/source/serializers.rst b/docs/source/serializers.rst
index 533ea366..1b3148ed 100644
--- a/docs/source/serializers.rst
+++ b/docs/source/serializers.rst
@@ -7,7 +7,7 @@ under the hood. It is documented just so you know they exist and can be used if
 
 .. note::
     Note that serializer's ``to_json`` methods ignore read-only fields of the objects.
-    Read only fields of the objects are ones that are passed to the parameters of their ``__init__`` with
+    Read-only fields of the objects are ones that are passed to the parameters of their ``__init__`` with
     underscores, e.g. ``Event(_updated=25/Nov/2020)``.
 
 Events serializer
@@ -450,5 +450,162 @@ To object
 
 
 
+Calendars serializer
+~~~~~~~~~~~~~~~~~~~~
+
+To json
+-------
+
+.. code-block:: python
+
+    from gcsa.calendar import Calendar, AccessRoles
+    from gcsa.serializers.calendar_serializer import CalendarSerializer
+
+    calendar = Calendar(
+        summary='Primary',
+        calendar_id='primary',
+        description='Description',
+        location='Location',
+        timezone='Timezone',
+        allowed_conference_solution_types=[
+            AccessRoles.FREE_BUSY_READER,
+            AccessRoles.READER,
+            AccessRoles.WRITER,
+            AccessRoles.OWNER,
+        ]
+    )
+
+    CalendarSerializer.to_json(calendar)
+
+.. code-block:: javascript
+
+    {
+        'id': 'primary',
+        'summary': 'Primary',
+        'description': 'Description',
+        'location': 'Location',
+        'timeZone': 'Timezone',
+        'conferenceProperties': {
+            'allowedConferenceSolutionTypes': [
+                'freeBusyReader',
+                'reader',
+                'writer',
+                'owner'
+            ]
+        }
+    }
+
+
+To object
+---------
+
+.. code-block:: python
+
+    calendar_json = {
+        'id': 'primary',
+        'summary': 'Primary',
+        'description': 'Description',
+        'location': 'Location',
+        'timeZone': 'Timezone',
+        'conferenceProperties': {
+            'allowedConferenceSolutionTypes': [
+                'freeBusyReader',
+                'reader',
+                'writer',
+                'owner'
+            ]
+        }
+    }
+    CalendarSerializer.to_object(calendar_json)
+
+.. code-block:: python
+
+    <Calendar Primary - Description>
+
+
+
+CalendarListEntry serializer
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To json
+-------
+
+.. code-block:: python
+
+    from gcsa.calendar import CalendarListEntry, NotificationType
+    from gcsa.reminders import EmailReminder
+    from gcsa.serializers.calendar_serializer import CalendarListEntrySerializer
+
+    calendar_list_entry = CalendarListEntry(
+        calendar_id='<calendar_id>',
+        summary_override='Holidays in Czechia 2022',
+        color_id='2',
+        background_color='#123456',
+        foreground_color='#234567',
+        hidden=True,
+        selected=False,
+        default_reminders=[EmailReminder(minutes_before_start=15)],
+        notification_types=[
+            NotificationType.EVENT_CREATION,
+            NotificationType.EVENT_CHANGE
+        ]
+    )
+
+    CalendarListEntrySerializer.to_json(calendar_list_entry)
+
+.. code-block:: javascript
+
+    {
+        'id': '<calendar_id>',
+        'summaryOverride': 'Holidays in Czechia 2022',
+        'colorId': '2',
+        'backgroundColor': '#123456',
+        'foregroundColor': '#234567',
+        'hidden': True,
+        'selected': False,
+        'defaultReminders': [
+            {'method': 'email', 'minutes': 15}
+        ],
+        'notificationSettings': {
+            'notifications': [
+                {'type': 'eventCreation', 'method': 'email'},
+                {'type': 'eventChange', 'method': 'email'}
+            ]
+        }
+    }
+
+
+To object
+---------
+
+.. code-block:: python
+
+    calendar_list_entry_json = {
+        'id': '<calendar_id>',
+        'summary': 'Státní svátky v ČR',
+        'summaryOverride': 'Holidays in Czechia 2022',
+        'colorId': '2',
+        'backgroundColor': '#123456',
+        'foregroundColor': '#234567',
+        'hidden': True,
+        'selected': False,
+        'defaultReminders': [
+            {'method': 'email', 'minutes': 15}
+        ],
+        'notificationSettings': {
+            'notifications': [
+                {'type': 'eventCreation', 'method': 'email'},
+                {'type': 'eventChange', 'method': 'email'}
+            ]
+        }
+    }
+
+    CalendarListEntrySerializer.to_object(calendar_list_entry_json)
+
+
+.. code-block:: python
+
+    <CalendarListEntry Holidays in Czechia 2022 - (Státní svátky v ČR)>
+
 
 .. _`official API documentation`: https://developers.google.com/calendar
diff --git a/docs/source/settings.rst b/docs/source/settings.rst
new file mode 100644
index 00000000..492bbaee
--- /dev/null
+++ b/docs/source/settings.rst
@@ -0,0 +1,43 @@
+.. _settings:
+
+Settings
+========
+
+You can retrieve user's settings for the given account with :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_settings`.
+
+To do so, create a :py:class:`~gcsa.google_calendar.GoogleCalendar` instance (see :ref:`getting_started` to get your
+credentials):
+
+.. code-block:: python
+
+    from gcsa.google_calendar import GoogleCalendar
+
+    gc = GoogleCalendar()
+
+
+Following code will return a corresponding :py:class:`~gcsa.settings.Settings` object:
+
+.. code-block:: python
+
+    from gcsa.google_calendar import GoogleCalendar
+
+    gc = GoogleCalendar()
+    settings = gc.get_settings()
+    print(settings)
+
+.. code-block:: python
+
+        User settings:
+        auto_add_hangouts=true
+        date_field_order=DMY
+        default_event_length=60
+        format24_hour_time=false
+        hide_invitations=false
+        hide_weekends=false
+        locale=en
+        remind_on_responded_events_only=false
+        show_declined_events=true
+        timezone=Europe/Prague
+        use_keyboard_shortcuts=true
+        week_start=1
+
diff --git a/docs/source/why_gcsa.rst b/docs/source/why_gcsa.rst
index b7824e22..5e442a41 100644
--- a/docs/source/why_gcsa.rst
+++ b/docs/source/why_gcsa.rst
@@ -23,7 +23,7 @@ If you'd like to try this yourself, here's the code you need:
     from gcsa.event import Event
     from beautiful_date import D, drange, days, MO
 
-    calendar = GoogleCalendar()
+    gc = GoogleCalendar()
 
     PUSH_UPS_COUNT = [
         5, 5, 0, 5, 10, 0, 10,
@@ -44,7 +44,7 @@ If you'd like to try this yourself, here's the code you need:
             start=day,
             minutes_before_popup_reminder=5
         )
-        calendar.add_event(e)
+        gc.add_event(e)
 
 
 
diff --git a/gcsa/_services/__init__.py b/gcsa/_services/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gcsa/_services/authentication.py b/gcsa/_services/authentication.py
new file mode 100644
index 00000000..c94e2cce
--- /dev/null
+++ b/gcsa/_services/authentication.py
@@ -0,0 +1,120 @@
+import pickle
+import os.path
+from typing import List
+
+from googleapiclient import discovery
+from google_auth_oauthlib.flow import InstalledAppFlow
+from google.auth.transport.requests import Request
+from google.oauth2.credentials import Credentials
+
+
+class AuthenticatedService:
+    """Handles authentication of the `GoogleCalendar`"""
+
+    _READ_WRITE_SCOPES = 'https://www.googleapis.com/auth/calendar'
+    _LIST_ORDERS = ("startTime", "updated")
+
+    def __init__(
+            self,
+            *,
+            credentials: Credentials = None,
+            credentials_path: str = None,
+            token_path: str = None,
+            save_token: bool = True,
+            read_only: bool = False,
+            authentication_flow_host: str = 'localhost',
+            authentication_flow_port: int = 8080
+    ):
+        """
+        Specify ``credentials`` to use in requests or ``credentials_path`` and ``token_path`` to get credentials from.
+
+        :param credentials:
+                Credentials with token and refresh token.
+                If specified, ``credentials_path``, ``token_path``, and ``save_token`` are ignored.
+                If not specified, credentials are retrieved from "token.pickle" file (specified in ``token_path`` or
+                default path) or with authentication flow using secret from "credentials.json" (specified in
+                ``credentials_path`` or default path)
+        :param credentials_path:
+                Path to "credentials.json" file. Default: ~/.credentials
+        :param token_path:
+                Existing path to load the token from, or path to save the token after initial authentication flow.
+                Default: "token.pickle" in the same directory as the credentials_path
+        :param save_token:
+                Whether to pickle token after authentication flow for future uses
+        :param read_only:
+                If require read only access. Default: False
+        :param authentication_flow_host:
+                Host to receive response during authentication flow
+        :param authentication_flow_port:
+                Port to receive response during authentication flow
+        """
+
+        if credentials:
+            self.credentials = self._ensure_refreshed(credentials)
+        else:
+            credentials_path = credentials_path or self._get_default_credentials_path()
+            credentials_dir, credentials_file = os.path.split(credentials_path)
+            token_path = token_path or os.path.join(credentials_dir, 'token.pickle')
+            scopes = [self._READ_WRITE_SCOPES + ('.readonly' if read_only else '')]
+
+            self.credentials = self._get_credentials(
+                token_path,
+                credentials_dir,
+                credentials_file,
+                scopes,
+                save_token,
+                authentication_flow_host,
+                authentication_flow_port
+            )
+
+        self.service = discovery.build('calendar', 'v3', credentials=self.credentials)
+
+    @staticmethod
+    def _ensure_refreshed(
+            credentials: Credentials
+    ) -> Credentials:
+        if not credentials.valid and credentials.expired and credentials.refresh_token:
+            credentials.refresh(Request())
+        return credentials
+
+    @staticmethod
+    def _get_credentials(
+            token_path: str,
+            credentials_dir: str,
+            credentials_file: str,
+            scopes: List[str],
+            save_token: bool,
+            host: str,
+            port: int
+    ) -> Credentials:
+        credentials = None
+
+        if os.path.exists(token_path):
+            with open(token_path, 'rb') as token_file:
+                credentials = pickle.load(token_file)
+
+        if not credentials or not credentials.valid:
+            if credentials and credentials.expired and credentials.refresh_token:
+                credentials.refresh(Request())
+            else:
+                credentials_path = os.path.join(credentials_dir, credentials_file)
+                flow = InstalledAppFlow.from_client_secrets_file(credentials_path, scopes)
+                credentials = flow.run_local_server(host=host, port=port)
+
+            if save_token:
+                with open(token_path, 'wb') as token_file:
+                    pickle.dump(credentials, token_file)
+
+        return credentials
+
+    @staticmethod
+    def _get_default_credentials_path() -> str:
+        """Checks if ".credentials" folder in home directory exists. If not, creates it.
+        :return: expanded path to .credentials folder
+        """
+        home_dir = os.path.expanduser('~')
+        credential_dir = os.path.join(home_dir, '.credentials')
+        if not os.path.exists(credential_dir):
+            os.makedirs(credential_dir)
+        credential_path = os.path.join(credential_dir, 'credentials.json')
+        return credential_path
diff --git a/gcsa/_services/base_service.py b/gcsa/_services/base_service.py
new file mode 100644
index 00000000..8256433f
--- /dev/null
+++ b/gcsa/_services/base_service.py
@@ -0,0 +1,40 @@
+from typing import Callable, Type
+
+from gcsa._services.authentication import AuthenticatedService
+
+
+class BaseService(AuthenticatedService):
+    def __init__(self, default_calendar, *args, **kwargs):
+        """
+        :param default_calendar:
+                Users email address or name/id of the calendar. Default: primary calendar of the user
+
+                If user's email or "primary" is specified, then primary calendar of the user is used.
+                You don't need to specify this parameter in this case as it is a default behaviour.
+
+                To use a different calendar you need to specify its id.
+                Go to calendar's `settings and sharing` -> `Integrate calendar` -> `Calendar ID`.
+        """
+        super().__init__(*args, **kwargs)
+        self.default_calendar = default_calendar
+
+    @staticmethod
+    def _list_paginated(
+            request_method: Callable,
+            serializer_cls: Type = None,
+            **kwargs
+    ):
+        page_token = None
+        while True:
+            response_json = request_method(
+                **kwargs,
+                pageToken=page_token
+            ).execute()
+            for item_json in response_json['items']:
+                if serializer_cls:
+                    yield serializer_cls(item_json).get_object()
+                else:
+                    yield item_json
+            page_token = response_json.get('nextPageToken')
+            if not page_token:
+                break
diff --git a/gcsa/_services/calendar_lists_service.py b/gcsa/_services/calendar_lists_service.py
new file mode 100644
index 00000000..bebf33d0
--- /dev/null
+++ b/gcsa/_services/calendar_lists_service.py
@@ -0,0 +1,133 @@
+from typing import Iterable, Union
+
+from gcsa._services.base_service import BaseService
+from gcsa.calendar import CalendarListEntry, Calendar
+from gcsa.serializers.calendar_serializer import CalendarListEntrySerializer
+
+
+class CalendarListService(BaseService):
+    """Calendar list management methods of the `GoogleCalendar`"""
+
+    def get_calendar_list(
+            self,
+            min_access_role: str = None,
+            show_deleted: bool = False,
+            show_hidden: bool = False
+    ) -> Iterable[CalendarListEntry]:
+        """Returns the calendars on the user's calendar list.
+
+        :param min_access_role:
+                The minimum access role for the user in the returned entries. See :py:class:`~gcsa.calendar.AccessRoles`
+                The default is no restriction.
+        :param show_deleted:
+                Whether to include deleted calendar list entries in the result. The default is False.
+        :param show_hidden:
+                Whether to show hidden entries. The default is False.
+
+        :return:
+                Iterable of :py:class:`~gcsa.calendar.CalendarListEntry` objects.
+        """
+        yield from self._list_paginated(
+            self.service.calendarList().list,
+            serializer_cls=CalendarListEntrySerializer,
+            minAccessRole=min_access_role,
+            showDeleted=show_deleted,
+            showHidden=show_hidden,
+        )
+
+    def get_calendar_list_entry(
+            self,
+            calendar_id: str = None
+    ) -> CalendarListEntry:
+        """Returns a calendar with the corresponding calendar_id from the user's calendar list.
+
+        :param calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+
+        :return:
+                The corresponding :py:class:`~gcsa.calendar.CalendarListEntry` object.
+        """
+        calendar_id = calendar_id or self.default_calendar
+        calendar_resource = self.service.calendarList().get(calendarId=calendar_id).execute()
+        return CalendarListEntrySerializer.to_object(calendar_resource)
+
+    def add_calendar_list_entry(
+            self,
+            calendar: CalendarListEntry,
+            color_rgb_format: bool = None
+    ) -> CalendarListEntry:
+        """Adds an existing calendar into the user's calendar list.
+
+        :param calendar:
+                :py:class:`~gcsa.calendar.CalendarListEntry` object.
+        :param color_rgb_format:
+                Whether to use the `foreground_color` and `background_color` fields to write the calendar colors (RGB).
+                If this feature is used, the index-based `color_id` field will be set to the best matching option
+                automatically. The default is True if `foreground_color` or `background_color` is set, False otherwise.
+
+        :return:
+                Created `CalendarListEntry` object with id.
+        """
+        if color_rgb_format is None:
+            color_rgb_format = (calendar.foreground_color is not None) or (calendar.background_color is not None)
+
+        body = CalendarListEntrySerializer.to_json(calendar)
+        calendar_json = self.service.calendarList().insert(
+            body=body,
+            colorRgbFormat=color_rgb_format
+        ).execute()
+        return CalendarListEntrySerializer.to_object(calendar_json)
+
+    def update_calendar_list_entry(
+            self,
+            calendar: CalendarListEntry,
+            color_rgb_format: bool = None
+    ) -> CalendarListEntry:
+        """Updates an existing calendar on the user's calendar list.
+
+        :param calendar:
+                :py:class:`~gcsa.calendar.Calendar` object with set `calendar_id`
+        :param color_rgb_format:
+                Whether to use the `foreground_color` and `background_color` fields to write the calendar colors (RGB).
+                If this feature is used, the index-based color_id field will be set to the best matching option
+                automatically. The default is True if `foreground_color` or `background_color` is set, False otherwise.
+
+        :return:
+                Updated calendar list entry object
+        """
+        if color_rgb_format is None:
+            color_rgb_format = calendar.foreground_color is not None or calendar.background_color is not None
+
+        body = CalendarListEntrySerializer.to_json(calendar)
+        calendar_json = self.service.calendarList().update(
+            calendarId=calendar.id,
+            body=body,
+            colorRgbFormat=color_rgb_format
+        ).execute()
+        return CalendarListEntrySerializer.to_object(calendar_json)
+
+    def delete_calendar_list_entry(
+            self,
+            calendar: Union[Calendar, CalendarListEntry, str]
+    ):
+        """Removes a calendar from the user's calendar list.
+
+        :param calendar:
+                Calendar's ID or :py:class:`~gcsa.calendar.Calendar`/:py:class:`~gcsa.calendar.CalendarListEntry` object
+                with the set `calendar_id`.
+        """
+        if isinstance(calendar, (Calendar, CalendarListEntry)):
+            if calendar.id is None:
+                raise ValueError("CalendarListEntry has to have calendar_id to be deleted.")
+            calendar_id = calendar.id
+        elif isinstance(calendar, str):
+            calendar_id = calendar
+        else:
+            raise TypeError(
+                '"calendar" object must me Calendar, CalendarListEntry, or str, not {!r}'.format(
+                    calendar.__class__.__name__
+                )
+            )
+        self.service.calendarList().delete(calendarId=calendar_id).execute()
diff --git a/gcsa/_services/calendars_service.py b/gcsa/_services/calendars_service.py
new file mode 100644
index 00000000..7af82b7d
--- /dev/null
+++ b/gcsa/_services/calendars_service.py
@@ -0,0 +1,109 @@
+from typing import Union
+
+from gcsa._services.base_service import BaseService
+from gcsa.calendar import Calendar, CalendarListEntry
+from gcsa.serializers.calendar_serializer import CalendarSerializer
+
+
+class CalendarsService(BaseService):
+    """Calendars management methods of the `GoogleCalendar`"""
+
+    def get_calendar(
+            self,
+            calendar_id: str = None
+    ) -> Calendar:
+        """Returns the calendar with the corresponding calendar_id.
+
+        :param calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+
+        :return:
+                The corresponding :py:class:`~gcsa.calendar.Calendar` object.
+        """
+        calendar_id = calendar_id or self.default_calendar
+        calendar_resource = self.service.calendars().get(
+            calendarId=calendar_id
+        ).execute()
+        return CalendarSerializer.to_object(calendar_resource)
+
+    def add_calendar(
+            self,
+            calendar: Calendar
+    ):
+        """Creates a secondary calendar.
+
+        :param calendar:
+                Calendar object.
+        :return:
+                Created calendar object with ID.
+        """
+        body = CalendarSerializer.to_json(calendar)
+        calendar_json = self.service.calendars().insert(
+            body=body
+        ).execute()
+        return CalendarSerializer.to_object(calendar_json)
+
+    def update_calendar(
+            self,
+            calendar: Calendar
+    ):
+        """Updates metadata for a calendar.
+
+        :param calendar:
+                Calendar object with set `calendar_id`
+
+        :return:
+                Updated calendar object
+        """
+        body = CalendarSerializer.to_json(calendar)
+        calendar_json = self.service.calendars().update(
+            calendarId=calendar.id,
+            body=body
+        ).execute()
+        return CalendarSerializer.to_object(calendar_json)
+
+    def delete_calendar(
+            self,
+            calendar: Union[Calendar, str]
+    ):
+        """Deletes a secondary calendar.
+
+        Use :py:meth:`~gcsa.google_calendar.GoogleCalendar.clear_calendar` for clearing all events on primary calendars.
+
+        :param calendar:
+                Calendar's ID or :py:class:`~gcsa.calendar.Calendar` object with set `calendar_id`.
+        """
+        if isinstance(calendar, (Calendar, CalendarListEntry)):
+            if calendar.id is None:
+                raise ValueError("Calendar has to have calendar_id to be deleted.")
+            calendar_id = calendar.id
+        elif isinstance(calendar, str):
+            calendar_id = calendar
+        else:
+            raise TypeError('"calendar" object must me Calendar or str, not {!r}'.format(calendar.__class__.__name__))
+
+        self.service.calendars().delete(calendarId=calendar_id).execute()
+
+    def clear_calendar(self):
+        """Clears a **primary** calendar.
+        This operation deletes all events associated with the **primary** calendar of an account.
+
+        Currently, there is no way to clear a secondary calendar.
+        You can use :py:meth:`~gcsa.google_calendar.GoogleCalendar.delete_event` method with the secondary calendar's ID
+        to delete events from a secondary calendar.
+        """
+        self.service.calendars().clear(calendarId='primary').execute()
+
+    def clear(self):
+        """Kept for back-compatibility. Use :py:meth:`~gcsa.google_calendar.GoogleCalendar.clear_calendar` instead.
+
+        Clears a **primary** calendar.
+        This operation deletes all events associated with the **primary** calendar of an account.
+
+        Currently, there is no way to clear a secondary calendar.
+        You can use :py:meth:`~gcsa.google_calendar.GoogleCalendar.delete_event` method with the secondary calendar's ID
+        to delete events from a secondary calendar.
+        """
+        self.clear_calendar()
diff --git a/gcsa/_services/colors_service.py b/gcsa/_services/colors_service.py
new file mode 100644
index 00000000..44161d73
--- /dev/null
+++ b/gcsa/_services/colors_service.py
@@ -0,0 +1,15 @@
+from gcsa._services.base_service import BaseService
+
+
+class ColorsService(BaseService):
+    """Colors management methods of the `GoogleCalendar`"""
+
+    def list_event_colors(self) -> dict:
+        """A global palette of event colors, mapping from the color ID to its definition.
+        An :py:class:`~gcsa.event.Event` may refer to one of these color IDs in its color_id field."""
+        return self.service.colors().get().execute()['event']
+
+    def list_calendar_colors(self) -> dict:
+        """A global palette of calendar colors, mapping from the color ID to its definition.
+        :py:class:`~gcsa.calendar.CalendarListEntry` resource refers to one of these color IDs in its color_id field."""
+        return self.service.colors().get().execute()['calendar']
diff --git a/gcsa/_services/events_service.py b/gcsa/_services/events_service.py
new file mode 100644
index 00000000..21e74e15
--- /dev/null
+++ b/gcsa/_services/events_service.py
@@ -0,0 +1,433 @@
+from datetime import date, datetime
+from typing import Union, Iterator, Iterable, Callable
+
+from beautiful_date import BeautifulDate
+from dateutil.relativedelta import relativedelta
+from tzlocal import get_localzone_name
+
+from gcsa._services.base_service import BaseService
+from gcsa.event import Event
+from gcsa.serializers.event_serializer import EventSerializer
+from gcsa.util.date_time_util import ensure_localisation
+
+
+class SendUpdatesMode:
+    """Possible values of the mode for sending updates or invitations to attendees.
+
+    * ALL - Send updates to all participants. This is the default value.
+    * EXTERNAL_ONLY - Send updates only to attendees not using google calendar.
+    * NONE - Do not send updates.
+    """
+
+    ALL = "all"
+    EXTERNAL_ONLY = "externalOnly"
+    NONE = "none"
+
+
+class EventsService(BaseService):
+    """Event management methods of the `GoogleCalendar`"""
+
+    def _list_events(
+            self,
+            request_method: Callable,
+            time_min: Union[date, datetime, BeautifulDate],
+            time_max: Union[date, datetime, BeautifulDate],
+            timezone: str,
+            calendar_id: str,
+            **kwargs
+    ) -> Iterable[Event]:
+        """Lists paginated events received from request_method."""
+
+        time_min = time_min or datetime.now()
+        time_max = time_max or time_min + relativedelta(years=1)
+
+        if not isinstance(time_min, datetime):
+            time_min = datetime.combine(time_min, datetime.min.time())
+
+        if not isinstance(time_max, datetime):
+            time_max = datetime.combine(time_max, datetime.max.time())
+
+        time_min = ensure_localisation(time_min, timezone).isoformat()
+        time_max = ensure_localisation(time_max, timezone).isoformat()
+
+        yield from self._list_paginated(
+            request_method,
+            serializer_cls=EventSerializer,
+            calendarId=calendar_id,
+            timeMin=time_min,
+            timeMax=time_max,
+            **kwargs
+        )
+
+    def get_events(
+            self,
+            time_min: Union[date, datetime, BeautifulDate] = None,
+            time_max: Union[date, datetime, BeautifulDate] = None,
+            order_by: str = None,
+            timezone: str = get_localzone_name(),
+            single_events: bool = False,
+            query: str = None,
+            calendar_id: str = None,
+            **kwargs
+    ) -> Iterable[Event]:
+        """Lists events.
+
+        :param time_min:
+                Staring date/datetime
+        :param time_max:
+                Ending date/datetime
+        :param order_by:
+                Order of the events. Possible values: "startTime", "updated". Default is unspecified stable order.
+        :param timezone:
+                Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
+                the computers local timezone is used if it is configured. UTC is used otherwise.
+        :param single_events:
+                Whether to expand recurring events into instances and only return single one-off events and
+                instances of recurring events, but not the underlying recurring events themselves.
+        :param query:
+                Free text search terms to find events that match these terms in any field, except for
+                extended properties.
+        :param calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+        :param kwargs:
+                Additional API parameters.
+                See https://developers.google.com/calendar/v3/reference/events/list#optional-parameters
+
+        :return:
+                Iterable of `Event` objects
+        """
+        calendar_id = calendar_id or self.default_calendar
+        if not single_events and order_by == 'startTime':
+            raise ValueError(
+                '"startTime" ordering is only available when querying single events, i.e. single_events=True'
+            )
+        yield from self._list_events(
+            self.service.events().list,
+            time_min=time_min,
+            time_max=time_max,
+            timezone=timezone,
+            calendar_id=calendar_id,
+            **{
+                'singleEvents': single_events,
+                'orderBy': order_by,
+                'q': query,
+                **kwargs
+            }
+        )
+
+    def get_instances(
+            self,
+            recurring_event,
+            time_min=None,
+            time_max=None,
+            timezone=get_localzone_name(),
+            calendar_id: str = None,
+            **kwargs
+    ) -> Iterable[Event]:
+        """Lists instances of recurring event
+
+        :param recurring_event:
+                Recurring event (Event object) or id of a recurring event
+        :param time_min:
+                Staring date/datetime
+        :param time_max:
+                Ending date/datetime
+        :param timezone:
+                Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
+                the computers local timezone is used if it is configured. UTC is used otherwise.
+        :param calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+        :param kwargs:
+                Additional API parameters.
+                See https://developers.google.com/calendar/v3/reference/events/instances#optional-parameters
+
+        :return:
+                Iterable of event objects
+        """
+        calendar_id = calendar_id or self.default_calendar
+        yield from self._list_events(
+            self.service.events().instances,
+            time_min=time_min,
+            time_max=time_max,
+            timezone=timezone,
+            calendar_id=calendar_id,
+            **{
+                'eventId': recurring_event if isinstance(recurring_event, str) else recurring_event.id,
+                **kwargs
+            }
+        )
+
+    def __iter__(self) -> Iterator[Event]:
+        return iter(self.get_events())
+
+    def __getitem__(self, r):
+        if isinstance(r, slice):
+            time_min, time_max, order_by = r.start or None, r.stop or None, r.step or None
+        elif isinstance(r, (date, datetime)):
+            time_min, time_max, order_by = r, None, None
+        else:
+            raise NotImplementedError
+
+        if (
+                (time_min and not isinstance(time_min, (date, datetime)))
+                or (time_max and not isinstance(time_max, (date, datetime)))
+                or (order_by and (not isinstance(order_by, str) or order_by not in self._LIST_ORDERS))
+        ):
+            raise ValueError('Calendar indexing is in the following format:  time_min[:time_max[:order_by]],'
+                             ' where time_min and time_max are date/datetime objects'
+                             ' and order_by is None or one of "startTime" or "updated" strings.')
+
+        return self.get_events(time_min, time_max, order_by=order_by, single_events=(order_by == "startTime"))
+
+    def get_event(
+            self,
+            event_id: str,
+            calendar_id: str = None,
+            **kwargs
+    ) -> Event:
+        """Returns the event with the corresponding event_id.
+
+        :param event_id:
+                The unique event ID.
+        :param kwargs:
+                Additional API parameters.
+                See https://developers.google.com/calendar/v3/reference/events/get#optional-parameters
+        :param calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+
+        :return:
+                The corresponding event object.
+        """
+        calendar_id = calendar_id or self.default_calendar
+        event_resource = self.service.events().get(
+            calendarId=calendar_id,
+            eventId=event_id,
+            **kwargs
+        ).execute()
+        return EventSerializer.to_object(event_resource)
+
+    def add_event(
+            self,
+            event: Event,
+            send_updates: str = SendUpdatesMode.NONE,
+            calendar_id: str = None,
+            **kwargs
+    ) -> Event:
+        """Creates event in the calendar
+
+        :param event:
+                Event object.
+        :param send_updates:
+                Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
+                Default is "NONE".
+        :param calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+        :param kwargs:
+                Additional API parameters.
+                See https://developers.google.com/calendar/v3/reference/events/insert#optional-parameters
+
+        :return:
+                Created event object with id.
+        """
+        calendar_id = calendar_id or self.default_calendar
+        body = EventSerializer.to_json(event)
+        event_json = self.service.events().insert(
+            calendarId=calendar_id,
+            body=body,
+            conferenceDataVersion=1,
+            sendUpdates=send_updates,
+            **kwargs
+        ).execute()
+        return EventSerializer.to_object(event_json)
+
+    def add_quick_event(
+            self,
+            event_string: str,
+            send_updates: str = SendUpdatesMode.NONE,
+            calendar_id: str = None,
+            **kwargs
+    ) -> Event:
+        """Creates event in the calendar by string description.
+
+        Example:
+            Appointment at Somewhere on June 3rd 10am-10:25am
+
+        :param event_string:
+                String that describes an event
+        :param send_updates:
+                Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
+                Default is "NONE".
+        :param calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+        :param kwargs:
+                Additional API parameters.
+                See https://developers.google.com/calendar/v3/reference/events/quickAdd#optional-parameters
+
+        :return:
+                Created event object with id.
+        """
+        calendar_id = calendar_id or self.default_calendar
+        event_json = self.service.events().quickAdd(
+            calendarId=calendar_id,
+            text=event_string,
+            sendUpdates=send_updates,
+            **kwargs
+        ).execute()
+        return EventSerializer.to_object(event_json)
+
+    def update_event(
+            self,
+            event: Event,
+            send_updates: str = SendUpdatesMode.NONE,
+            calendar_id: str = None,
+            **kwargs
+    ) -> Event:
+        """Updates existing event in the calendar
+
+        :param event:
+                Event object with set `event_id`.
+        :param send_updates:
+                Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
+                Default is "NONE".
+        :param calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+        :param kwargs:
+                Additional API parameters.
+                See https://developers.google.com/calendar/v3/reference/events/update#optional-parameters
+
+        :return:
+                Updated event object.
+        """
+        calendar_id = calendar_id or self.default_calendar
+        body = EventSerializer.to_json(event)
+        event_json = self.service.events().update(
+            calendarId=calendar_id,
+            eventId=event.id,
+            body=body,
+            conferenceDataVersion=1,
+            sendUpdates=send_updates,
+            **kwargs
+        ).execute()
+        return EventSerializer.to_object(event_json)
+
+    def import_event(
+            self,
+            event: Event,
+            calendar_id: str = None,
+            **kwargs
+    ) -> Event:
+        """Imports an event in the calendar
+
+        This operation is used to add a private copy of an existing event to a calendar.
+
+        :param event:
+                Event object.
+        :param calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+        :param kwargs:
+                Additional API parameters.
+                See https://developers.google.com/calendar/v3/reference/events/import#optional-parameters
+
+        :return:
+                Created event object with id.
+        """
+        calendar_id = calendar_id or self.default_calendar
+        body = EventSerializer.to_json(event)
+        event_json = self.service.events().import_(
+            calendarId=calendar_id,
+            body=body,
+            conferenceDataVersion=1,
+            **kwargs
+        ).execute()
+        return EventSerializer.to_object(event_json)
+
+    def move_event(
+            self,
+            event: Event,
+            destination_calendar_id: str,
+            send_updates: str = SendUpdatesMode.NONE,
+            source_calendar_id: str = None,
+            **kwargs
+    ) -> Event:
+        """Moves existing event from calendar to another calendar
+
+        :param event:
+                Event object with set event_id.
+        :param destination_calendar_id:
+                ID of the destination calendar.
+        :param send_updates:
+                Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
+                Default is "NONE".
+        :param source_calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+        :param kwargs:
+                Additional API parameters.
+                See https://developers.google.com/calendar/v3/reference/events/move#optional-parameters
+
+        :return:
+                Moved event object.
+        """
+        source_calendar_id = source_calendar_id or self.default_calendar
+        moved_event_json = self.service.events().move(
+            calendarId=source_calendar_id,
+            eventId=event.id,
+            destination=destination_calendar_id,
+            sendUpdates=send_updates,
+            **kwargs
+        ).execute()
+        return EventSerializer.to_object(moved_event_json)
+
+    def delete_event(
+            self,
+            event: Union[Event, str],
+            send_updates: str = SendUpdatesMode.NONE,
+            calendar_id: str = None,
+            **kwargs
+    ):
+        """Deletes an event.
+
+        :param event:
+                Event's ID or `Event` object with set `event_id`.
+        :param send_updates:
+                Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
+                Default is "NONE".
+        :param calendar_id:
+                Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+                If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
+        :param kwargs:
+                Additional API parameters.
+                See https://developers.google.com/calendar/v3/reference/events/delete#optional-parameters
+        """
+        calendar_id = calendar_id or self.default_calendar
+        if isinstance(event, Event):
+            if event.id is None:
+                raise ValueError("Event has to have event_id to be deleted.")
+            event_id = event.id
+        elif isinstance(event, str):
+            event_id = event
+        else:
+            raise TypeError('"event" object must be Event or str, not {!r}'.format(event.__class__.__name__))
+
+        self.service.events().delete(
+            calendarId=calendar_id,
+            eventId=event_id,
+            sendUpdates=send_updates,
+            **kwargs
+        ).execute()
diff --git a/gcsa/_services/settings_service.py b/gcsa/_services/settings_service.py
new file mode 100644
index 00000000..e7986a40
--- /dev/null
+++ b/gcsa/_services/settings_service.py
@@ -0,0 +1,13 @@
+from gcsa._services.base_service import BaseService
+from gcsa.serializers.settings_serializer import SettingsSerializer
+from gcsa.settings import Settings
+
+
+class SettingsService(BaseService):
+    """Settings management methods of the `GoogleCalendar`"""
+
+    def get_settings(self) -> Settings:
+        """Returns user settings for the authenticated user."""
+        settings_list = list(self._list_paginated(self.service.settings().list))
+        settings_json = {s['id']: s['value'] for s in settings_list}
+        return SettingsSerializer.to_object(settings_json)
diff --git a/gcsa/attachment.py b/gcsa/attachment.py
index fdc0dc27..1e8151bc 100644
--- a/gcsa/attachment.py
+++ b/gcsa/attachment.py
@@ -18,7 +18,14 @@ class Attachment:
         "application/vnd.google-apps.drive-sdk"  # 3rd party shortcut
     }
 
-    def __init__(self, file_url, title=None, mime_type=None, _icon_link=None, _file_id=None):
+    def __init__(
+            self,
+            file_url: str,
+            title: str = None,
+            mime_type: str = None,
+            _icon_link: str = None,
+            _file_id: str = None
+    ):
         """File attachment for the event.
 
         Currently only Google Drive attachments are supported.
diff --git a/gcsa/attendee.py b/gcsa/attendee.py
index 4696f1ff..a0fbe098 100644
--- a/gcsa/attendee.py
+++ b/gcsa/attendee.py
@@ -1,4 +1,4 @@
-from gcsa.person import Person
+from .person import Person
 
 
 class ResponseStatus:
@@ -16,16 +16,18 @@ class ResponseStatus:
 
 
 class Attendee(Person):
-    def __init__(self,
-                 email,
-                 display_name=None,
-                 comment=None,
-                 optional=None,
-                 is_resource=None,
-                 additional_guests=None,
-                 _id=None,
-                 _is_self=None,
-                 _response_status=None):
+    def __init__(
+            self,
+            email: str,
+            display_name: str = None,
+            comment: str = None,
+            optional: bool = None,
+            is_resource: bool = None,
+            additional_guests: int = None,
+            _id: str = None,
+            _is_self: bool = None,
+            _response_status: str = None
+    ):
         """Represents attendee of the event.
 
         :param email:
diff --git a/gcsa/calendar.py b/gcsa/calendar.py
new file mode 100644
index 00000000..44279d9f
--- /dev/null
+++ b/gcsa/calendar.py
@@ -0,0 +1,255 @@
+from typing import List
+
+from tzlocal import get_localzone_name
+
+from .reminders import Reminder
+
+
+class NotificationType:
+    """
+    * `EVENT_CREATION` - Notification sent when a new event is put on the calendar.
+    * `EVENT_CHANGE` - Notification sent when an event is changed.
+    * `EVENT_CANCELLATION` - Notification sent when an event is cancelled.
+    * `EVENT_RESPONSE` - Notification sent when an attendee responds to the event invitation.
+    * `AGENDA` - An agenda with the events of the day (sent out in the morning).
+    """
+
+    EVENT_CREATION = "eventCreation"
+    EVENT_CHANGE = "eventChange"
+    EVENT_CANCELLATION = "eventCancellation"
+    EVENT_RESPONSE = "eventResponse"
+    AGENDA = "agenda"
+
+
+class AccessRoles:
+    """
+    * `FREE_BUSY_READER` - Provides read access to free/busy information.
+    * `READER` - Provides read access to the calendar.
+      Private events will appear to users with reader access, but event details will be hidden.
+    * `WRITER` - Provides read and write access to the calendar.
+      Private events will appear to users with writer access, and event details will be visible.
+    * `OWNER` - Provides ownership of the calendar.
+      This role has all of the permissions of the writer role with the additional ability to see and manipulate ACLs.
+    """
+
+    FREE_BUSY_READER = "freeBusyReader"
+    READER = "reader"
+    WRITER = "writer"
+    OWNER = "owner"
+
+
+class Calendar:
+    def __init__(
+            self,
+            summary: str,
+            *,
+            calendar_id: str = None,
+            description: str = None,
+            location: str = None,
+            timezone: str = get_localzone_name(),
+            allowed_conference_solution_types: List[str] = None
+    ):
+        """
+        :param summary:
+                Title of the calendar.
+        :param calendar_id:
+                Identifier of the calendar.
+                To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
+        :param description:
+                Description of the calendar.
+        :param location:
+                Geographic location of the calendar as free-form text.
+        :param timezone:
+                Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
+                the computers local timezone is used if it is configured. UTC is used otherwise.
+        :param allowed_conference_solution_types:
+                The types of conference solutions that are supported for this calendar.
+                See :py:class:`~gcsa.conference.SolutionType`
+        """
+        self.summary = summary
+        self.calendar_id = calendar_id
+        self.description = description
+        self.location = location
+        self.timezone = timezone
+        self.allowed_conference_solution_types = allowed_conference_solution_types
+
+    @property
+    def id(self):
+        return self.calendar_id
+
+    def to_calendar_list_entry(
+            self,
+            summary_override: str = None,
+            color_id: str = None,
+            background_color: str = None,
+            foreground_color: str = None,
+            hidden: bool = False,
+            selected: bool = False,
+            default_reminders: List[Reminder] = None,
+            notification_types: List[str] = None,
+    ) -> 'CalendarListEntry':
+        """Converts :py:class:`~gcsa.calendar.Calendar` to :py:class:`~gcsa.calendar.CalendarListEntry`
+        that can be added to the calendar list.
+
+        :py:class:`~gcsa.calendar.Calendar` has to have `calendar_id` set
+        to be converted to :py:class:`~gcsa.calendar.CalendarListEntry`
+
+        :param summary_override:
+                The summary that the authenticated user has set for this calendar.
+        :param color_id:
+                The color of the calendar. This is an ID referring to an entry in the calendar section of the colors'
+                definition (See :py:meth:`~gcsa.google_calendar.GoogleCalendar.list_calendar_colors`).
+                This property is superseded by the `background_color` and `foreground_color` properties
+                and can be ignored when using these properties.
+        :param background_color:
+                The main color of the calendar in the hexadecimal format "#0088aa".
+                This property supersedes the index-based color_id property.
+        :param foreground_color:
+                The foreground color of the calendar in the hexadecimal format "#ffffff".
+                This property supersedes the index-based color_id property.
+        :param hidden:
+                Whether the calendar has been hidden from the list.
+        :param selected:
+                Whether the calendar content shows up in the calendar UI. The default is False.
+        :param default_reminders:
+                The default reminders that the authenticated user has for this calendar. :py:mod:`~gcsa.reminders`
+        :param notification_types:
+                The list of notification types set for this calendar. :py:class:`~gcsa:calendar:NotificationType`
+
+        :return:
+                :py:class:`~gcsa.calendar.CalendarListEntry` object that can be added to the calendar list.
+        """
+        if self.id is None:
+            raise ValueError('Calendar has to have `calendar_id` set to be converted to CalendarListEntry')
+
+        return CalendarListEntry(
+            _summary=self.summary,
+            calendar_id=self.calendar_id,
+            _description=self.description,
+            _location=self.location,
+            _timezone=self.timezone,
+            _allowed_conference_solution_types=self.allowed_conference_solution_types,
+
+            summary_override=summary_override,
+            color_id=color_id,
+            background_color=background_color,
+            foreground_color=foreground_color,
+            hidden=hidden,
+            selected=selected,
+            default_reminders=default_reminders,
+            notification_types=notification_types,
+        )
+
+    def __str__(self):
+        return '{} - {}'.format(self.summary, self.description)
+
+    def __repr__(self):
+        return '<Calendar {}>'.format(self.__str__())
+
+    def __eq__(self, other):
+        if not isinstance(other, Calendar):
+            return NotImplemented
+        elif self is other:
+            return True
+        else:
+            return super().__eq__(other)
+
+
+class CalendarListEntry(Calendar):
+    def __init__(
+            self,
+            calendar_id: str,
+            *,
+            summary_override: str = None,
+            color_id: str = None,
+            background_color: str = None,
+            foreground_color: str = None,
+            hidden: bool = False,
+            selected: bool = False,
+            default_reminders: List[Reminder] = None,
+            notification_types: List[str] = None,
+            _summary: str = None,
+            _description: str = None,
+            _location: str = None,
+            _timezone: str = None,
+            _allowed_conference_solution_types: List[str] = None,
+            _access_role: str = None,
+            _primary: bool = False,
+            _deleted: bool = False
+    ):
+        """
+        :param calendar_id:
+                Identifier of the calendar.
+        :param summary_override:
+                The summary that the authenticated user has set for this calendar.
+        :param color_id:
+                The color of the calendar. This is an ID referring to an entry in the calendar section of the colors'
+                definition (See :py:meth:`~gcsa.google_calendar.GoogleCalendar.list_calendar_colors`).
+                This property is superseded by the `background_color` and `foreground_color` properties
+                and can be ignored when using these properties.
+        :param background_color:
+                The main color of the calendar in the hexadecimal format "#0088aa".
+                This property supersedes the index-based color_id property.
+        :param foreground_color:
+                The foreground color of the calendar in the hexadecimal format "#ffffff".
+                This property supersedes the index-based color_id property.
+        :param hidden:
+                Whether the calendar has been hidden from the list.
+        :param selected:
+                Whether the calendar content shows up in the calendar UI. The default is False.
+        :param default_reminders:
+                The default reminders that the authenticated user has for this calendar. :py:mod:`~gcsa.reminders`
+        :param notification_types:
+                The list of notification types set for this calendar. :py:class:`~gcsa:calendar:NotificationType`
+        :param _summary:
+                Title of the calendar. Read-only.
+        :param _description:
+                Description of the calendar. Read-only.
+        :param _location:
+                Geographic location of the calendar as free-form text. Read-only.
+        :param _timezone:
+                Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". Read-only.
+        :param _allowed_conference_solution_types:
+                The types of conference solutions that are supported for this calendar. Read-only.
+                See :py:class:`~gcsa.conference.SolutionType`
+        :param _access_role:
+                The effective access role that the authenticated user has on the calendar. Read-only.
+                See :py:class:`~gcsa.calendar.AccessRoles`
+        :param _primary:
+                Whether the calendar is the primary calendar of the authenticated user. Read-only.
+        :param _deleted:
+                Whether this calendar list entry has been deleted from the calendar list. Read-only.
+        """
+        super().__init__(
+            summary=_summary,
+            calendar_id=calendar_id,
+            description=_description,
+            location=_location,
+            timezone=_timezone,
+            allowed_conference_solution_types=_allowed_conference_solution_types
+        )
+        self.summary_override = summary_override
+        self.color_id = color_id
+        self.background_color = background_color
+        self.foreground_color = foreground_color
+        self.hidden = hidden
+        self.selected = selected
+        self.default_reminders = default_reminders
+        self.notification_types = notification_types
+        self.access_role = _access_role
+        self.primary = _primary
+        self.deleted = _deleted
+
+    def __str__(self):
+        return '{} - ({})'.format(self.summary_override, self.summary)
+
+    def __repr__(self):
+        return '<CalendarListEntry {}>'.format(self.__str__())
+
+    def __eq__(self, other):
+        if not isinstance(other, CalendarListEntry):
+            return NotImplemented
+        elif self is other:
+            return True
+        else:
+            return super().__eq__(other)
diff --git a/gcsa/conference.py b/gcsa/conference.py
index 7d765e7a..c46c8de2 100644
--- a/gcsa/conference.py
+++ b/gcsa/conference.py
@@ -1,3 +1,4 @@
+from typing import Union, List
 from uuid import uuid4
 
 
@@ -20,10 +21,10 @@ class _BaseConferenceSolution:
 
     def __init__(
             self,
-            conference_id=None,
-            signature=None,
-            notes=None,
-            _status='success'
+            conference_id: str = None,
+            signature: str = None,
+            notes: str = None,
+            _status: str = 'success'
     ):
         """
         :param conference_id:
@@ -54,9 +55,9 @@ def __init__(
 
                 * "pending": the conference create request is still being processed.
                 * "failure": the conference create request failed, there are no entry points.
-                * | "success": the conference create request succeeded, the entry points are populated.
-                  | In this case `ConferenceSolution` with created entry points
-                    is stored in the event's `conference_data`. And `ConferenceSolutionCreateRequest` is omitted.
+                * "success": the conference create request succeeded, the entry points are populated.
+                  In this case `ConferenceSolution` with created entry points
+                  is stored in the event's `conference_data`. And `ConferenceSolutionCreateRequest` is omitted.
 
                 Create requests are asynchronous. Check ``status`` field of event's ``conference_solution`` to find it's
                 status. If the status is ``"success"``, ``conference_solution`` will contain a
@@ -98,14 +99,14 @@ class EntryPoint:
 
     def __init__(
             self,
-            entry_point_type,
-            uri=None,
-            label=None,
-            pin=None,
-            access_code=None,
-            meeting_code=None,
-            passcode=None,
-            password=None
+            entry_point_type: str,
+            uri: str = None,
+            label: str = None,
+            pin: str = None,
+            access_code: str = None,
+            meeting_code: str = None,
+            passcode: str = None,
+            password: str = None
     ):
         """
         When creating new conference data, populate only the subset of `meeting_code`, `access_code`, `passcode`,
@@ -118,25 +119,25 @@ def __init__(
 
                 Possible values are:
 
-                * | VIDEO - joining a conference over HTTP.
-                  | A conference can have zero or one `VIDEO` entry point.
-                * | PHONE - joining a conference by dialing a phone number.
-                  | A conference can have zero or more `PHONE` entry points.
-                * | SIP - joining a conference over SIP.
-                  | A conference can have zero or one `SIP` entry point.
-                * | MORE - further conference joining instructions, for example additional phone numbers.
-                  | A conference can have zero or one `MORE` entry point.
-                  | A conference with only a `MORE` entry point is not a valid conference.
+                * VIDEO - joining a conference over HTTP.
+                  A conference can have zero or one `VIDEO` entry point.
+                * PHONE - joining a conference by dialing a phone number.
+                  A conference can have zero or more `PHONE` entry points.
+                * SIP - joining a conference over SIP.
+                  A conference can have zero or one `SIP` entry point.
+                * MORE - further conference joining instructions, for example additional phone numbers.
+                  A conference can have zero or one `MORE` entry point.
+                  A conference with only a `MORE` entry point is not a valid conference.
 
         :param uri:
                 The URI of the entry point. The maximum length is 1300 characters.
                 Format:
 
-                * | for `VIDEO`, http: or https: schema is required.
-                * | for `PHONE`, tel: schema is required.
-                  | The URI should include the entire dial sequence (e.g., tel:+12345678900,,,123456789;1234).
-                * | for `SIP`, sip: schema is required, e.g., sip:12345678@myprovider.com.
-                * | for `MORE`, http: or https: schema is required.
+                * for `VIDEO`, http: or https: schema is required.
+                * for `PHONE`, tel: schema is required.
+                  The URI should include the entire dial sequence (e.g., tel:+12345678900,,,123456789;1234).
+                * for `SIP`, sip: schema is required, e.g., sip:12345678@myprovider.com.
+                * for `MORE`, http: or https: schema is required.
 
         :param label:
                 The label for the URI.
@@ -217,13 +218,13 @@ class ConferenceSolution(_BaseConferenceSolution):
 
     def __init__(
             self,
-            entry_points,
-            solution_type=None,
-            name=None,
-            icon_uri=None,
-            conference_id=None,
-            signature=None,
-            notes=None
+            entry_points: Union[EntryPoint, List[EntryPoint]],
+            solution_type: str = None,
+            name: str = None,
+            icon_uri: str = None,
+            conference_id: str = None,
+            signature: str = None,
+            notes: str = None
     ):
         """
         :param entry_points:
@@ -336,12 +337,12 @@ class ConferenceSolutionCreateRequest(_BaseConferenceSolution):
 
     def __init__(
             self,
-            solution_type=None,
-            request_id=None,
-            _status=None,
-            conference_id=None,
-            signature=None,
-            notes=None
+            solution_type: str = None,
+            request_id: str = None,
+            _status: str = None,
+            conference_id: str = None,
+            signature: str = None,
+            notes: str = None
     ):
         """
         :param solution_type:
@@ -367,9 +368,9 @@ def __init__(
 
                 * "pending": the conference create request is still being processed.
                 * "failure": the conference create request failed, there are no entry points.
-                * | "success": the conference create request succeeded, the entry points are populated.
-                  | In this case `ConferenceSolution` with created entry points
-                    is stored in the event's `conference_data`. And `ConferenceSolutionCreateRequest` is omitted.
+                * "success": the conference create request succeeded, the entry points are populated.
+                  In this case `ConferenceSolution` with created entry points
+                  is stored in the event's `conference_data`. And `ConferenceSolutionCreateRequest` is omitted.
         :param conference_id:
                 The ID of the conference. Optional.
                 Can be used by developers to keep track of conferences, should not be displayed to users.
diff --git a/gcsa/event.py b/gcsa/event.py
index a5188a70..a37469f5 100644
--- a/gcsa/event.py
+++ b/gcsa/event.py
@@ -1,21 +1,24 @@
 from functools import total_ordering
+from typing import List, Union
 
 from beautiful_date import BeautifulDate
-from tzlocal import get_localzone
+from tzlocal import get_localzone_name
 from datetime import datetime, date, timedelta
 
 from .attachment import Attachment
 from .attendee import Attendee
-from .reminders import PopupReminder, EmailReminder
-from .util.date_time_util import insure_localisation
+from .conference import ConferenceSolution, ConferenceSolutionCreateRequest
+from .person import Person
+from .reminders import PopupReminder, EmailReminder, Reminder
+from .util.date_time_util import ensure_localisation
 
 
 class Visibility:
-    """ Possible values of the event visibility.
+    """Possible values of the event visibility.
 
-    * DEFAULT - Uses the default visibility for events on the calendar. This is the default value.
-    * PUBLIC - The event is public and event details are visible to all readers of the calendar.
-    * PRIVATE - The event is private and only event attendees may view event details.
+    * `DEFAULT` - Uses the default visibility for events on the calendar. This is the default value.
+    * `PUBLIC` - The event is public and event details are visible to all readers of the calendar.
+    * `PRIVATE` - The event is private and only event attendees may view event details.
     """
 
     DEFAULT = "default"
@@ -24,12 +27,12 @@ class Visibility:
 
 
 class Transparency:
-    """ Possible values of the event transparency.
+    """Possible values of the event transparency.
 
-    * OPAQUE - Default value. The event does block time on the calendar.
-               This is equivalent to setting 'Show me as' to 'Busy' in the Calendar UI.
-    * TRANSPARENT - The event does not block time on the calendar.
-                    This is equivalent to setting 'Show me as' to 'Available' in the Calendar UI.
+    * `OPAQUE` - Default value. The event does block time on the calendar.
+      This is equivalent to setting 'Show me as' to 'Busy' in the Calendar UI.
+    * `TRANSPARENT` - The event does not block time on the calendar.
+      This is equivalent to setting 'Show me as' to 'Available' in the Calendar UI.
     """
 
     OPAQUE = 'opaque'
@@ -38,35 +41,37 @@ class Transparency:
 
 @total_ordering
 class Event:
-    def __init__(self,
-                 summary,
-                 start,
-                 end=None,
-                 *,
-                 timezone=str(get_localzone()),
-                 event_id=None,
-                 description=None,
-                 location=None,
-                 recurrence=None,
-                 color_id=None,
-                 visibility=Visibility.DEFAULT,
-                 attendees=None,
-                 attachments=None,
-                 conference_solution=None,
-                 reminders=None,
-                 default_reminders=False,
-                 minutes_before_popup_reminder=None,
-                 minutes_before_email_reminder=None,
-                 guests_can_invite_others=True,
-                 guests_can_modify=False,
-                 guests_can_see_other_guests=True,
-                 transparency=None,
-                 _creator=None,
-                 _organizer=None,
-                 _created=None,
-                 _updated=None,
-                 _recurring_event_id=None,
-                 **other):
+    def __init__(
+            self,
+            summary: str,
+            start: Union[date, datetime, BeautifulDate],
+            end: Union[date, datetime, BeautifulDate] = None,
+            *,
+            timezone: str = get_localzone_name(),
+            event_id: str = None,
+            description: str = None,
+            location: str = None,
+            recurrence: Union[str, List[str]] = None,
+            color_id: str = None,
+            visibility: str = Visibility.DEFAULT,
+            attendees: Union[Attendee, str, List[Attendee], List[str]] = None,
+            attachments: Union[Attachment, List[Attachment]] = None,
+            conference_solution: Union[ConferenceSolution, ConferenceSolutionCreateRequest] = None,
+            reminders: Union[Reminder, List[Reminder]] = None,
+            default_reminders: bool = False,
+            minutes_before_popup_reminder: int = None,
+            minutes_before_email_reminder: int = None,
+            guests_can_invite_others: bool = True,
+            guests_can_modify: bool = False,
+            guests_can_see_other_guests: bool = True,
+            transparency: str = None,
+            _creator: Person = None,
+            _organizer: Person = None,
+            _created: datetime = None,
+            _updated: datetime = None,
+            _recurring_event_id: str = None,
+            **other
+    ):
         """
         :param summary:
                 Title of the event.
@@ -79,7 +84,7 @@ def __init__(self,
                 Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
                 the computers local timezone is used if it is configured. UTC is used otherwise.
         :param event_id:
-                Opaque identifier of the event. By default is generated by the server. You can specify id as a
+                Opaque identifier of the event. By default, it is generated by the server. You can specify id as a
                 5-1024 long string of characters used in base32hex ([a-vA-V0-9]). The ID must be unique per
                 calendar.
         :param description:
@@ -89,7 +94,8 @@ def __init__(self,
         :param recurrence:
                 RRULE/RDATE/EXRULE/EXDATE string or list of such strings. See :py:mod:`~gcsa.recurrence`
         :param color_id:
-                Color id referring to an entry from colors endpoint (list_event_colors)
+                Color id referring to an entry from colors endpoint.
+                See :py:meth:`~gcsa.google_calendar.GoogleCalendar.list_event_colors`
         :param visibility:
                 Visibility of the event. Default is default visibility for events on the calendar.
                 See :py:class:`~gcsa.event.Visibility`
@@ -137,7 +143,7 @@ def __init__(self,
                 See more in https://developers.google.com/calendar/v3/reference/events
         """
 
-        def assure_list(obj):
+        def ensure_list(obj):
             return [] if obj is None else obj if isinstance(obj, list) else [obj]
 
         self.timezone = timezone
@@ -150,8 +156,8 @@ def assure_list(obj):
             self.end = start + timedelta(days=1)
 
         if isinstance(self.start, datetime) and isinstance(self.end, datetime):
-            self.start = insure_localisation(self.start, timezone)
-            self.end = insure_localisation(self.end, timezone)
+            self.start = ensure_localisation(self.start, timezone)
+            self.end = ensure_localisation(self.end, timezone)
         elif isinstance(self.start, datetime) or isinstance(self.end, datetime):
             raise TypeError('Start and end must either both be date or both be datetime.')
 
@@ -168,8 +174,8 @@ def insure_date(d):
         self.created = _created
         self.updated = _updated
 
-        attendees = [self._ensure_attendee_from_email(a) for a in assure_list(attendees)]
-        reminders = assure_list(reminders)
+        attendees = [self._ensure_attendee_from_email(a) for a in ensure_list(attendees)]
+        reminders = ensure_list(reminders)
 
         if len(reminders) > 5:
             raise ValueError('The maximum number of override reminders is 5.')
@@ -181,11 +187,11 @@ def insure_date(d):
         self.summary = summary
         self.description = description
         self.location = location
-        self.recurrence = assure_list(recurrence)
+        self.recurrence = ensure_list(recurrence)
         self.color_id = color_id
         self.visibility = visibility
         self.attendees = attendees
-        self.attachments = assure_list(attachments)
+        self.attachments = ensure_list(attachments)
         self.conference_solution = conference_solution
         self.reminders = reminders
         self.default_reminders = default_reminders
@@ -208,31 +214,50 @@ def insure_date(d):
     def id(self):
         return self.event_id
 
-    def add_attendee(self, attendee):
+    def add_attendee(
+            self,
+            attendee: Union[str, Attendee]
+    ):
         """Adds attendee to an event. See :py:class:`~gcsa.attendee.Attendee`.
         Attendee may be given as email string or :py:class:`~gcsa.attendee.Attendee` object."""
         self.attendees.append(self._ensure_attendee_from_email(attendee))
 
-    def add_attachment(self, file_url, title=None, mime_type=None):
+    def add_attachment(
+            self,
+            file_url: str,
+            title: str = None,
+            mime_type: str = None
+    ):
         """Adds attachment to an event. See :py:class:`~gcsa.attachment.Attachment`"""
         self.attachments.append(Attachment(file_url=file_url, title=title, mime_type=mime_type))
 
-    def add_email_reminder(self, minutes_before_start=60):
+    def add_email_reminder(
+            self,
+            minutes_before_start: int = 60
+    ):
         """Adds email reminder to an event. See :py:class:`~gcsa.reminders.EmailReminder`"""
         self.add_reminder(EmailReminder(minutes_before_start))
 
-    def add_popup_reminder(self, minutes_before_start=30):
+    def add_popup_reminder(
+            self,
+            minutes_before_start: int = 30
+    ):
         """Adds popup reminder to an event. See :py:class:`~gcsa.reminders.PopupReminder`"""
         self.add_reminder(PopupReminder(minutes_before_start))
 
-    def add_reminder(self, reminder):
+    def add_reminder(
+            self,
+            reminder: Reminder
+    ):
         """Adds reminder to an event. See :py:mod:`~gcsa.reminders`"""
         if len(self.reminders) > 4:
             raise ValueError('The maximum number of override reminders is 5.')
         self.reminders.append(reminder)
 
     @staticmethod
-    def _ensure_attendee_from_email(attendee_or_email):
+    def _ensure_attendee_from_email(
+            attendee_or_email: Union[str, Attendee]
+    ):
         """If attendee_or_email is email string, returns created :py:class:`~gcsa.attendee.Attendee`
         object with the given email."""
         if isinstance(attendee_or_email, str):
@@ -253,7 +278,7 @@ def __repr__(self):
     def __lt__(self, other):
         def insure_datetime(d, timezone):
             if type(d) == date:
-                return insure_localisation(datetime(year=d.year, month=d.month, day=d.day), timezone)
+                return ensure_localisation(datetime(year=d.year, month=d.month, day=d.day), timezone)
             else:
                 return d
 
diff --git a/gcsa/google_calendar.py b/gcsa/google_calendar.py
index 5781051a..b89cfd08 100644
--- a/gcsa/google_calendar.py
+++ b/gcsa/google_calendar.py
@@ -1,56 +1,37 @@
-from datetime import date, datetime
-import pickle
-import os.path
-from typing import List, Union, Callable, Iterable, Iterator
-
-from gcsa.event import Event
-from beautiful_date import BeautifulDate
-from dateutil.relativedelta import relativedelta
-from googleapiclient import discovery
-from google_auth_oauthlib.flow import InstalledAppFlow
-from google.auth.transport.requests import Request
-from tzlocal import get_localzone
-
 from google.oauth2.credentials import Credentials
 
-from .serializers.event_serializer import EventSerializer
-from .util.date_time_util import insure_localisation
-
-
-class SendUpdatesMode:
-    """ Possible values of the mode for sending updates or invitations to attendees.
-
-    * ALL - Send updates to all participants. This is the default value.
-    * EXTERNAL_ONLY - Send updates only to attendees not using google calendar.
-    * NONE - Do not send updates.
-    """
-
-    ALL = "all"
-    EXTERNAL_ONLY = "externalOnly"
-    NONE = "none"
+from ._services.events_service import EventsService, SendUpdatesMode  # noqa: F401
+from ._services.calendars_service import CalendarsService
+from ._services.calendar_lists_service import CalendarListService
+from ._services.colors_service import ColorsService
+from ._services.settings_service import SettingsService
 
 
-class GoogleCalendar:
-    _READ_WRITE_SCOPES = 'https://www.googleapis.com/auth/calendar'
-    _LIST_ORDERS = ("startTime", "updated")
+class GoogleCalendar(
+    EventsService,
+    CalendarsService,
+    CalendarListService,
+    ColorsService,
+    SettingsService
+):
+    """Collection of all supported methods for events and calendars management."""
 
     def __init__(
             self,
-            calendar: str = 'primary',
+            default_calendar: str = 'primary',
             *,
             credentials: Credentials = None,
             credentials_path: str = None,
             token_path: str = None,
             save_token: bool = True,
             read_only: bool = False,
-            authentication_flow_host='localhost',
-            authentication_flow_port=8080
+            authentication_flow_host: str = 'localhost',
+            authentication_flow_port: int = 8080
     ):
-        """Represents Google Calendar of the user.
-
+        """
         Specify ``credentials`` to use in requests or ``credentials_path`` and ``token_path`` to get credentials from.
 
-        :param calendar:
+        :param default_calendar:
                 Users email address or name/id of the calendar. Default: primary calendar of the user
 
                 If user's email or "primary" is specified, then primary calendar of the user is used.
@@ -78,430 +59,13 @@ def __init__(
         :param authentication_flow_port:
                 Port to receive response during authentication flow
         """
-
-        if credentials:
-            self.credentials = self._assure_refreshed(credentials)
-        else:
-            credentials_path = credentials_path or GoogleCalendar._get_default_credentials_path()
-            credentials_dir, credentials_file = os.path.split(credentials_path)
-            token_path = token_path or os.path.join(credentials_dir, 'token.pickle')
-            scopes = [self._READ_WRITE_SCOPES + ('.readonly' if read_only else '')]
-
-            self.credentials = self._get_credentials(
-                token_path,
-                credentials_dir,
-                credentials_file,
-                scopes,
-                save_token,
-                authentication_flow_host,
-                authentication_flow_port
-            )
-
-        self.calendar = calendar
-        self.service = discovery.build('calendar', 'v3', credentials=self.credentials)
-
-    @staticmethod
-    def _assure_refreshed(credentials: Credentials):
-        if not credentials.valid and credentials.expired and credentials.refresh_token:
-            credentials.refresh(Request())
-        return credentials
-
-    @staticmethod
-    def _get_credentials(
-            token_path: str,
-            credentials_dir: str,
-            credentials_file: str,
-            scopes: List[str],
-            save_token: bool,
-            host: str,
-            port: int
-    ) -> Credentials:
-        credentials = None
-
-        if os.path.exists(token_path):
-            with open(token_path, 'rb') as token_file:
-                credentials = pickle.load(token_file)
-
-        if not credentials or not credentials.valid:
-            if credentials and credentials.expired and credentials.refresh_token:
-                credentials.refresh(Request())
-            else:
-                credentials_path = os.path.join(credentials_dir, credentials_file)
-                flow = InstalledAppFlow.from_client_secrets_file(credentials_path, scopes)
-                credentials = flow.run_local_server(host=host, port=port)
-
-            if save_token:
-                with open(token_path, 'wb') as token_file:
-                    pickle.dump(credentials, token_file)
-
-        return credentials
-
-    @staticmethod
-    def _get_default_credentials_path() -> str:
-        """ Checks if ".credentials" folder in home directory exists. If not, creates it.
-        :return: expanded path to .credentials folder
-        """
-        home_dir = os.path.expanduser('~')
-        credential_dir = os.path.join(home_dir, '.credentials')
-        if not os.path.exists(credential_dir):
-            os.makedirs(credential_dir)
-        credential_path = os.path.join(credential_dir, 'credentials.json')
-        return credential_path
-
-    def clear(self):
-        """Deletes all the events in the calendar"""
-        self.service.calendars().clear(calendarId=self.calendar).execute()
-
-    def add_event(
-            self,
-            event,
-            send_updates=SendUpdatesMode.NONE,
-            **kwargs
-    ) -> Event:
-        """Creates event in the calendar
-
-        :param event:
-                Event object.
-        :param send_updates:
-                Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
-                Default is "NONE".
-        :param kwargs:
-                Additional API parameters.
-                See https://developers.google.com/calendar/v3/reference/events/insert#optional-parameters
-
-        :return:
-                Created event object with id.
-        """
-        body = EventSerializer(event).get_json()
-        event_json = self.service.events().insert(
-            calendarId=self.calendar,
-            body=body,
-            conferenceDataVersion=1,
-            sendUpdates=send_updates,
-            **kwargs
-        ).execute()
-        return EventSerializer.to_object(event_json)
-
-    def add_quick_event(
-            self,
-            event_string,
-            send_updates=SendUpdatesMode.NONE,
-            **kwargs
-    ) -> Event:
-        """Creates event in the calendar by string description.
-
-        Example:
-            Appointment at Somewhere on June 3rd 10am-10:25am
-
-        :param event_string:
-                String that describes an event
-        :param send_updates:
-                Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
-                Default is "NONE".
-        :param kwargs:
-                Additional API parameters.
-                See https://developers.google.com/calendar/v3/reference/events/quickAdd#optional-parameters
-
-        :return:
-                Created event object with id.
-        """
-        event_json = self.service.events().quickAdd(
-            calendarId=self.calendar,
-            text=event_string,
-            sendUpdates=send_updates,
-            **kwargs
-        ).execute()
-        return EventSerializer.to_object(event_json)
-
-    def import_event(self, event, **kwargs) -> Event:
-        """Imports an event in the calendar
-
-        This operation is used to add a private copy of an existing event to a calendar.
-
-        :param event:
-                Event object.
-        :param kwargs:
-                Additional API parameters.
-                See https://developers.google.com/calendar/v3/reference/events/import#optional-parameters
-
-        :return:
-                Created event object with id.
-        """
-        body = EventSerializer(event).get_json()
-        event_json = self.service.events().import_(
-            calendarId=self.calendar,
-            body=body,
-            conferenceDataVersion=1,
-            **kwargs
-        ).execute()
-        return EventSerializer.to_object(event_json)
-
-    def update_event(
-            self,
-            event,
-            send_updates=SendUpdatesMode.NONE,
-            **kwargs
-    ) -> Event:
-        """Updates existing event in the calendar
-
-        :param event:
-                Event object with set event_id.
-        :param send_updates:
-                Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
-                Default is "NONE".
-        :param kwargs:
-                Additional API parameters.
-                See https://developers.google.com/calendar/v3/reference/events/update#optional-parameters
-
-        :return:
-                Updated event object.
-        """
-        body = EventSerializer(event).get_json()
-        event_json = self.service.events().update(
-            calendarId=self.calendar,
-            eventId=event.id,
-            body=body,
-            conferenceDataVersion=1,
-            sendUpdates=send_updates,
-            **kwargs
-        ).execute()
-        return EventSerializer.to_object(event_json)
-
-    def move_event(
-            self,
-            event,
-            destination_calendar_id,
-            send_updates=SendUpdatesMode.NONE,
-            **kwargs
-    ) -> Event:
-        """Moves existing event from calendar to another calendar
-
-        :param event:
-                Event object with set event_id.
-        :param destination_calendar_id:
-                Id of the destination calendar.
-        :param send_updates:
-                Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
-                Default is "NONE".
-        :param kwargs:
-                Additional API parameters.
-                See https://developers.google.com/calendar/v3/reference/events/move#optional-parameters
-
-        :return:
-                Moved event object.
-        """
-        moved_event_json = self.service.events().move(
-            calendarId=self.calendar,
-            eventId=event.id,
-            destination=destination_calendar_id,
-            sendUpdates=send_updates,
-            **kwargs
-        ).execute()
-        return EventSerializer.to_object(moved_event_json)
-
-    def delete_event(
-            self,
-            event: Union[Event, str],
-            send_updates=SendUpdatesMode.NONE,
-            **kwargs
-    ):
-        """ Deletes an event.
-
-        :param event:
-                Event object with set event_id.
-        :param send_updates:
-                Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
-                Default is "NONE".
-        :param kwargs:
-                Additional API parameters.
-                See https://developers.google.com/calendar/v3/reference/events/delete#optional-parameters
-        """
-        if isinstance(event, Event):
-            if event.id is None:
-                raise ValueError("Event has to have event_id to be deleted.")
-            event_id = event.id
-        elif isinstance(event, str):
-            event_id = event
-        else:
-            raise TypeError('"event" object must me Event or str, not {!r}'.format(event.__class__.__name__))
-
-        self.service.events().delete(
-            calendarId=self.calendar,
-            eventId=event_id,
-            sendUpdates=send_updates,
-            **kwargs
-        ).execute()
-
-    def _list_events(
-            self,
-            request_method: Callable,
-            time_min: Union[date, datetime, BeautifulDate],
-            time_max: Union[date, datetime, BeautifulDate],
-            timezone: str,
-            **kwargs
-    ) -> Iterable[Event]:
-        """Lists paginated events received from request_method."""
-
-        time_min = time_min or datetime.now()
-        time_max = time_max or time_min + relativedelta(years=1)
-
-        if not isinstance(time_min, datetime):
-            time_min = datetime.combine(time_min, datetime.min.time())
-
-        if not isinstance(time_max, datetime):
-            time_max = datetime.combine(time_max, datetime.max.time())
-
-        time_min = insure_localisation(time_min, timezone).isoformat()
-        time_max = insure_localisation(time_max, timezone).isoformat()
-
-        page_token = None
-        while True:
-            events = request_method(
-                calendarId=self.calendar,
-                timeMin=time_min,
-                timeMax=time_max,
-                pageToken=page_token,
-                **kwargs
-            ).execute()
-            for event_json in events['items']:
-                event = EventSerializer(event_json).get_object()
-                yield event
-            page_token = events.get('nextPageToken')
-            if not page_token:
-                break
-
-    def get_events(
-            self,
-            time_min=None,
-            time_max=None,
-            order_by=None,
-            timezone=str(get_localzone()),
-            single_events=False,
-            query=None,
-            **kwargs
-    ) -> Iterable[Event]:
-        """ Lists events
-
-        :param time_min:
-                Staring date/datetime
-        :param time_max:
-                Ending date/datetime
-        :param order_by:
-                Order of the events. Possible values: "startTime", "updated". Default is unspecified stable order.
-        :param timezone:
-                Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
-                the computers local timezone is used if it is configured. UTC is used otherwise.
-        :param single_events:
-                Whether to expand recurring events into instances and only return single one-off events and
-                instances of recurring events, but not the underlying recurring events themselves.
-        :param query:
-                Free text search terms to find events that match these terms in any field, except for
-                extended properties.
-        :param kwargs:
-                Additional API parameters.
-                See https://developers.google.com/calendar/v3/reference/events/list#optional-parameters
-
-        :return:
-                Iterable of event objects
-        """
-
-        if not single_events and order_by == 'startTime':
-            raise ValueError(
-                '"startTime" ordering is only available when querying single events, i.e. single_events=True'
-            )
-
-        yield from self._list_events(
-            self.service.events().list,
-            time_min=time_min,
-            time_max=time_max,
-            timezone=timezone,
-            **{
-                'singleEvents': single_events,
-                'orderBy': order_by,
-                'q': query,
-                **kwargs
-            }
-        )
-
-    def get_instances(
-            self,
-            recurring_event,
-            time_min=None,
-            time_max=None,
-            timezone=str(get_localzone()),
-            **kwargs
-    ) -> Iterable[Event]:
-        """ Lists instances of recurring event
-
-        :param recurring_event:
-                Recurring event (Event object) or id of a recurring event
-        :param time_min:
-                Staring date/datetime
-        :param time_max:
-                Ending date/datetime
-        :param timezone:
-                Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
-                the computers local timezone is used if it is configured. UTC is used otherwise.
-        :param kwargs:
-                Additional API parameters.
-                See https://developers.google.com/calendar/v3/reference/events/instances#optional-parameters
-
-        :return:
-                Iterable of event objects
-        """
-        yield from self._list_events(
-            self.service.events().instances,
-            time_min=time_min,
-            time_max=time_max,
-            timezone=timezone,
-            **{
-                'eventId': recurring_event if isinstance(recurring_event, str) else recurring_event.id,
-                **kwargs
-            }
+        super().__init__(
+            default_calendar=default_calendar,
+            credentials=credentials,
+            credentials_path=credentials_path,
+            token_path=token_path,
+            save_token=save_token,
+            read_only=read_only,
+            authentication_flow_host=authentication_flow_host,
+            authentication_flow_port=authentication_flow_port
         )
-
-    def get_event(self, event_id, **kwargs) -> Event:
-        """ Returns the event with the corresponding event_id.
-
-        :param event_id:
-                The unique event ID.
-        :param kwargs:
-                Additional API parameters.
-                See https://developers.google.com/calendar/v3/reference/events/get#optional-parameters
-
-        :return:
-                The corresponding event object or None if
-                no matching ID was found.
-        """
-        event_resource = self.service.events().get(
-            calendarId=self.calendar,
-            eventId=event_id,
-            **kwargs
-        ).execute()
-        return EventSerializer(event_resource).get_object()
-
-    def list_event_colors(self):
-        """List allowed event colors for the calendar."""
-        return self.service.colors().get().execute()['event']
-
-    def __iter__(self) -> Iterator[Event]:
-        return iter(self.get_events())
-
-    def __getitem__(self, r):
-        if isinstance(r, slice):
-            time_min, time_max, order_by = r.start or None, r.stop or None, r.step or None
-        elif isinstance(r, (date, datetime)):
-            time_min, time_max, order_by = r, None, None
-        else:
-            raise NotImplementedError
-
-        if (
-                (time_min and not isinstance(time_min, (date, datetime)))
-                or (time_max and not isinstance(time_max, (date, datetime)))
-                or (order_by and (not isinstance(order_by, str) or order_by not in self._LIST_ORDERS))
-        ):
-            raise ValueError('Calendar indexing is in the following format:  time_min[:time_max[:order_by]],'
-                             ' where time_min and time_max are date/datetime objects'
-                             ' and order_by is None or one of "startTime" or "updated" strings.')
-
-        return self.get_events(time_min, time_max, order_by=order_by, single_events=(order_by == "startTime"))
diff --git a/gcsa/person.py b/gcsa/person.py
index a323847e..046bd2c1 100644
--- a/gcsa/person.py
+++ b/gcsa/person.py
@@ -1,9 +1,11 @@
 class Person:
-    def __init__(self,
-                 email=None,
-                 display_name=None,
-                 _id=None,
-                 _is_self=None):
+    def __init__(
+            self,
+            email: str = None,
+            display_name: str = None,
+            _id: str = None,
+            _is_self: bool = None
+    ):
         """Represents organizer's, creator's, or primary attendee's fields.
         For attendees see more in :py:class:`~gcsa.attendee.Attendee`.
 
diff --git a/gcsa/recurrence.py b/gcsa/recurrence.py
index fe2c4b7f..c133e98a 100644
--- a/gcsa/recurrence.py
+++ b/gcsa/recurrence.py
@@ -1,8 +1,8 @@
 from datetime import datetime, date
 
-from tzlocal import get_localzone
+from tzlocal import get_localzone_name
 
-from .util.date_time_util import insure_localisation
+from .util.date_time_util import ensure_localisation
 
 
 class Duration:
@@ -244,7 +244,7 @@ def dates(ds):
         return 'RDATE;' + Recurrence._dates(ds)
 
     @staticmethod
-    def times(dts, timezone=str(get_localzone())):
+    def times(dts, timezone=get_localzone_name()):
         """Converts datetime(s) set to RDATE format.
 
         :param dts:
@@ -259,7 +259,7 @@ def times(dts, timezone=str(get_localzone())):
         return 'RDATE;' + Recurrence._times(dts, timezone)
 
     @staticmethod
-    def periods(ps, timezone=str(get_localzone())):
+    def periods(ps, timezone=get_localzone_name()):
         """Converts date period(s) to RDATE format.
 
         Period is defined as tuple of starting date/datetime and ending date/datetime or duration as Duration object:
@@ -289,7 +289,7 @@ def exclude_dates(ds):
         return 'EXDATE;' + Recurrence._dates(ds)
 
     @staticmethod
-    def exclude_times(dts, timezone=str(get_localzone())):
+    def exclude_times(dts, timezone=get_localzone_name()):
         """Converts datetime(s) set to EXDATE format.
 
         :param dts:
@@ -304,7 +304,7 @@ def exclude_times(dts, timezone=str(get_localzone())):
         return 'EXDATE;' + Recurrence._times(dts, timezone)
 
     @staticmethod
-    def exclude_periods(ps, timezone=str(get_localzone())):
+    def exclude_periods(ps, timezone=get_localzone_name()):
         """Converts date period(s) to EXDATE format.
 
         Period is defined as tuple of starting date/datetime and ending date/datetime or duration as Duration object:
@@ -322,7 +322,7 @@ def exclude_periods(ps, timezone=str(get_localzone())):
         return 'EXDATE;' + Recurrence._periods(ps, timezone)
 
     @staticmethod
-    def _times(dts, timezone=str(get_localzone())):
+    def _times(dts, timezone=get_localzone_name()):
         """Converts datetime(s) set to RDATE format.
 
         :param dts:
@@ -343,7 +343,7 @@ def _times(dts, timezone=str(get_localzone())):
             if not isinstance(dt, (date, datetime)):
                 msg = 'The dts object(s) must be date or datetime, not {!r}.'.format(dt.__class__.__name__)
                 raise TypeError(msg)
-            localized_datetimes.append(insure_localisation(dt, timezone))
+            localized_datetimes.append(ensure_localisation(dt, timezone))
 
         return 'TZID={}:{}'.format(timezone, ','.join(d.strftime('%Y%m%dT%H%M%S') for d in localized_datetimes))
 
@@ -368,7 +368,7 @@ def _dates(ds):
         return 'VALUE=DATE:' + ','.join(d.strftime('%Y%m%d') for d in ds)
 
     @staticmethod
-    def _periods(ps, timezone=str(get_localzone())):
+    def _periods(ps, timezone=get_localzone_name()):
         """Converts date period(s) to RDATE format.
 
         Period is defined as tuple of starting date/datetime and ending date/datetime or duration as Duration object:
@@ -392,9 +392,9 @@ def _periods(ps, timezone=str(get_localzone())):
                 msg = 'The start object(s) must be a date or datetime, not {!r}.'.format(end.__class__.__name__)
                 raise TypeError(msg)
 
-            start = insure_localisation(start, timezone)
+            start = ensure_localisation(start, timezone)
             if isinstance(end, (date, datetime)):
-                end = insure_localisation(end, timezone)
+                end = ensure_localisation(end, timezone)
                 pstr = '{}/{}'.format(start.strftime('%Y%m%dT%H%M%SZ'), end.strftime('%Y%m%dT%H%M%SZ'))
             elif isinstance(end, Duration):
                 pstr = '{}/{}'.format(start.strftime('%Y%m%dT%H%M%SZ'), end)
@@ -471,7 +471,7 @@ def _rule(
         .. _`RRULE format`: https://tools.ietf.org/html/rfc5545#section-3.8.5
         """
 
-        def assure_iterable(it):
+        def ensure_iterable(it):
             return it if isinstance(it, (list, tuple, set)) else [it] if it is not None else []
 
         def check_all_type(it, type_, name):
@@ -510,31 +510,31 @@ def to_string(values):
         if count is not None and until is not None:
             raise ValueError('"count" and "until" may not appear in one recurrence rule.')
 
-        by_second = assure_iterable(by_second)
+        by_second = ensure_iterable(by_second)
         check_all_type_and_range(by_second, int, (0, 60), "by_second")
 
-        by_minute = assure_iterable(by_minute)
+        by_minute = ensure_iterable(by_minute)
         check_all_type_and_range(by_minute, int, (0, 59), "by_minute")
 
-        by_hour = assure_iterable(by_hour)
+        by_hour = ensure_iterable(by_hour)
         check_all_type_and_range(by_hour, int, (0, 23), "by_hour")
 
-        by_week_day = assure_iterable(by_week_day)
+        by_week_day = ensure_iterable(by_week_day)
         check_all_type(by_week_day, _DayOfTheWeek, "by_week_day")
 
-        by_month_day = assure_iterable(by_month_day)
+        by_month_day = ensure_iterable(by_month_day)
         check_all_type_and_range(by_month_day, int, (-31, 31), "by_month_day", nonzero=True)
 
-        by_year_day = assure_iterable(by_year_day)
+        by_year_day = ensure_iterable(by_year_day)
         check_all_type_and_range(by_year_day, int, (-366, 366), "by_year_day", nonzero=True)
 
-        by_week = assure_iterable(by_week)
+        by_week = ensure_iterable(by_week)
         check_all_type_and_range(by_week, int, (-53, 53), "by_week", nonzero=True)
 
-        by_month = assure_iterable(by_month)
+        by_month = ensure_iterable(by_month)
         check_all_type_and_range(by_month, int, (1, 12), "by_month")
 
-        by_set_pos = assure_iterable(by_set_pos)
+        by_set_pos = ensure_iterable(by_set_pos)
         check_all_type_and_range(by_set_pos, int, (-366, 366), "by_set_pos", nonzero=True)
         if by_set_pos and all(not v for v in (by_second, by_minute, by_hour,
                                               by_week_day, by_month_day, by_year_day,
diff --git a/gcsa/reminders.py b/gcsa/reminders.py
index ccfedda0..b13dcdc6 100644
--- a/gcsa/reminders.py
+++ b/gcsa/reminders.py
@@ -1,5 +1,9 @@
 class Reminder:
-    def __init__(self, method, minutes_before_start):
+    def __init__(
+            self,
+            method: str,
+            minutes_before_start: int
+    ):
         """Represents base reminder object
 
         :param method:
@@ -25,7 +29,10 @@ def __repr__(self):
 
 
 class EmailReminder(Reminder):
-    def __init__(self, minutes_before_start=60):
+    def __init__(
+            self,
+            minutes_before_start: int = 60
+    ):
         """Represents email reminder object
 
         :param minutes_before_start:
@@ -35,7 +42,10 @@ def __init__(self, minutes_before_start=60):
 
 
 class PopupReminder(Reminder):
-    def __init__(self, minutes_before_start=30):
+    def __init__(
+            self,
+            minutes_before_start: int = 30
+    ):
         """Represents popup reminder object
 
         :param minutes_before_start:
diff --git a/gcsa/serializers/attendee_serializer.py b/gcsa/serializers/attendee_serializer.py
index 98d374a5..5a185811 100644
--- a/gcsa/serializers/attendee_serializer.py
+++ b/gcsa/serializers/attendee_serializer.py
@@ -1,5 +1,5 @@
 from gcsa.attendee import Attendee
-from gcsa.serializers.base_serializer import BaseSerializer
+from .base_serializer import BaseSerializer
 
 
 class AttendeeSerializer(BaseSerializer):
diff --git a/gcsa/serializers/base_serializer.py b/gcsa/serializers/base_serializer.py
index d86c0bcf..50f21b37 100644
--- a/gcsa/serializers/base_serializer.py
+++ b/gcsa/serializers/base_serializer.py
@@ -31,7 +31,7 @@ def _remove_empty_values(data):
 
     @classmethod
     def to_json(cls, obj):
-        cls.assure_type(obj)
+        cls.ensure_type(obj)
         return cls._to_json(obj)
 
     @staticmethod
@@ -41,7 +41,7 @@ def _to_json(obj):
 
     @classmethod
     def to_object(cls, json_):
-        json_ = cls.assure_dict(json_)
+        json_ = cls.ensure_dict(json_)
         return cls._to_object(json_)
 
     @staticmethod
@@ -50,7 +50,7 @@ def _to_object(json_):
         pass
 
     @staticmethod
-    def assure_dict(json_):
+    def ensure_dict(json_):
         if not isinstance(json_, (str, dict)):
             raise TypeError('The json object must be str or dict, not {!r}'.format(json_.__class__.__name__))
 
@@ -60,13 +60,13 @@ def assure_dict(json_):
             return json_
 
     @classmethod
-    def assure_type(cls, obj):
+    def ensure_type(cls, obj):
         if not isinstance(obj, cls.type_):
             raise TypeError('The object must be {}, not {!r}.'.format(cls.type_, obj.__class__.__name__))
 
     def __init_subclass__(cls, **kwargs):
         """Checks that "type_" is defined and that name of the argument in subclasses __init__ method is the name of
-        the "type_" in lowercase. It assures that error in __init__ function of BaseSerializer has a correct message.
+        the "type_" in lowercase. It ensures that error in __init__ function of BaseSerializer has a correct message.
         """
         if cls.type_ is None:
             raise AssertionError('Subclass of BaseSerializer has to define class "type_" that is being serialized.')
diff --git a/gcsa/serializers/calendar_serializer.py b/gcsa/serializers/calendar_serializer.py
new file mode 100644
index 00000000..b0f7fab0
--- /dev/null
+++ b/gcsa/serializers/calendar_serializer.py
@@ -0,0 +1,109 @@
+from gcsa.calendar import Calendar, CalendarListEntry
+from .base_serializer import BaseSerializer
+from .reminder_serializer import ReminderSerializer
+
+
+class CalendarSerializer(BaseSerializer):
+    type_ = Calendar
+
+    def __init__(self, calendar):
+        super().__init__(calendar)
+
+    @staticmethod
+    def _to_json(calendar: Calendar):
+        data = {
+            "id": calendar.calendar_id,
+            "summary": calendar.summary,
+            "description": calendar.description,
+            "location": calendar.location,
+            "timeZone": calendar.timezone,
+        }
+        if calendar.allowed_conference_solution_types:
+            data["conferenceProperties"] = {
+                "allowedConferenceSolutionTypes": calendar.allowed_conference_solution_types
+            }
+
+        data = CalendarSerializer._remove_empty_values(data)
+
+        return data
+
+    @staticmethod
+    def _to_object(json_calendar):
+        conference_properties = json_calendar.get('conferenceProperties', {})
+        allowed_conference_solution_types = conference_properties.get('allowedConferenceSolutionTypes')
+        return Calendar(
+            summary=json_calendar.get('summary'),
+            calendar_id=json_calendar.get('id'),
+            description=json_calendar.get('description'),
+            location=json_calendar.get('location'),
+            timezone=json_calendar.get('timeZone'),
+            allowed_conference_solution_types=allowed_conference_solution_types
+        )
+
+
+class CalendarListEntrySerializer(BaseSerializer):
+    type_ = CalendarListEntry
+
+    def __init__(self, calendar_list_entry):
+        super().__init__(calendar_list_entry)
+
+    @staticmethod
+    def _to_json(calendar: CalendarListEntry):
+        data = {
+            "id": calendar.calendar_id,
+            "summaryOverride": calendar.summary_override,
+            "colorId": calendar.color_id,
+            "backgroundColor": calendar.background_color,
+            "foregroundColor": calendar.foreground_color,
+            "hidden": calendar.hidden,
+            "selected": calendar.selected,
+
+        }
+        if calendar.default_reminders:
+            data["defaultReminders"] = [ReminderSerializer.to_json(r) for r in calendar.default_reminders]
+
+        if calendar.notification_types:
+            data["notificationSettings"] = {
+                "notifications": [
+                    {
+                        "type": notification_type,
+                        "method": "email"
+                    }
+                    for notification_type in calendar.notification_types
+                ]
+            }
+
+        data = CalendarListEntrySerializer._remove_empty_values(data)
+
+        return data
+
+    @staticmethod
+    def _to_object(json_calendar):
+        conference_properties = json_calendar.pop('conferenceProperties', {})
+        allowed_conference_solution_types = conference_properties.pop('allowedConferenceSolutionTypes', None)
+
+        reminders_json = json_calendar.pop('defaultReminders', [])
+        default_reminders = [ReminderSerializer.to_object(r) for r in reminders_json] if reminders_json else None
+
+        notifications = json_calendar.pop('notificationSettings', {}).pop('notifications', None)
+        notification_types = [n['type'] for n in notifications] if notifications else None
+
+        return CalendarListEntry(
+            calendar_id=json_calendar.pop('id'),
+            summary_override=json_calendar.pop('summaryOverride', None),
+            color_id=json_calendar.pop('colorId', None),
+            background_color=json_calendar.pop('backgroundColor', None),
+            foreground_color=json_calendar.pop('foregroundColor', None),
+            hidden=json_calendar.pop('hidden', False),
+            selected=json_calendar.pop('selected', False),
+            default_reminders=default_reminders,
+            notification_types=notification_types,
+            _summary=json_calendar.pop('summary', None),
+            _description=json_calendar.pop('description', None),
+            _location=json_calendar.pop('location', None),
+            _timezone=json_calendar.pop('timeZone', None),
+            _allowed_conference_solution_types=allowed_conference_solution_types,
+            _access_role=json_calendar.pop('accessRole', None),
+            _primary=json_calendar.pop('primary', False),
+            _deleted=json_calendar.pop('deleted', False)
+        )
diff --git a/gcsa/serializers/conference_serializer.py b/gcsa/serializers/conference_serializer.py
index 4d1e464a..fa5bb9d3 100644
--- a/gcsa/serializers/conference_serializer.py
+++ b/gcsa/serializers/conference_serializer.py
@@ -1,5 +1,5 @@
 from gcsa.conference import ConferenceSolutionCreateRequest, ConferenceSolution, EntryPoint
-from gcsa.serializers.base_serializer import BaseSerializer
+from .base_serializer import BaseSerializer
 
 
 class EntryPointSerializer(BaseSerializer):
diff --git a/gcsa/serializers/event_serializer.py b/gcsa/serializers/event_serializer.py
index f4734f74..f94b1fc1 100644
--- a/gcsa/serializers/event_serializer.py
+++ b/gcsa/serializers/event_serializer.py
@@ -2,12 +2,12 @@
 
 from datetime import date, datetime
 
-from tzlocal import get_localzone
+from tzlocal import get_localzone_name
 
 from gcsa.event import Event
+from .base_serializer import BaseSerializer
 from .attachment_serializer import AttachmentSerializer
 from .attendee_serializer import AttendeeSerializer
-from .base_serializer import BaseSerializer
 from .conference_serializer import ConferenceSolutionSerializer, ConferenceSolutionCreateRequestSerializer
 from .person_serializer import PersonSerializer
 from .reminder_serializer import ReminderSerializer
@@ -88,7 +88,7 @@ def _to_object(json_event):
                 start = EventSerializer._get_datetime_from_string(start_data['date']).date()
             else:
                 start = EventSerializer._get_datetime_from_string(start_data['dateTime'])
-            timezone = start_data.get('timeZone', str(get_localzone()))
+            timezone = start_data.get('timeZone', get_localzone_name())
 
         end = None
         end_data = json_event.pop('end', None)
diff --git a/gcsa/serializers/person_serializer.py b/gcsa/serializers/person_serializer.py
index 9bd82836..60d210e8 100644
--- a/gcsa/serializers/person_serializer.py
+++ b/gcsa/serializers/person_serializer.py
@@ -1,5 +1,5 @@
 from gcsa.person import Person
-from gcsa.serializers.base_serializer import BaseSerializer
+from .base_serializer import BaseSerializer
 
 
 class PersonSerializer(BaseSerializer):
diff --git a/gcsa/serializers/settings_serializer.py b/gcsa/serializers/settings_serializer.py
new file mode 100644
index 00000000..207b2441
--- /dev/null
+++ b/gcsa/serializers/settings_serializer.py
@@ -0,0 +1,44 @@
+from gcsa.settings import Settings
+from .base_serializer import BaseSerializer
+
+
+class SettingsSerializer(BaseSerializer):
+    type_ = Settings
+
+    def __init__(self, settings):
+        super().__init__(settings)
+
+    @staticmethod
+    def _to_json(settings: Settings):
+        """Isn't used as Settings are read-only"""
+        return {
+            'autoAddHangouts': settings.auto_add_hangouts,
+            'dateFieldOrder': settings.date_field_order,
+            'defaultEventLength': settings.default_event_length,
+            'format24HourTime': settings.format24_hour_time,
+            'hideInvitations': settings.hide_invitations,
+            'hideWeekends': settings.hide_weekends,
+            'locale': settings.locale,
+            'remindOnRespondedEventsOnly': settings.remind_on_responded_events_only,
+            'showDeclinedEvents': settings.show_declined_events,
+            'timezone': settings.timezone,
+            'useKeyboardShortcuts': settings.use_keyboard_shortcuts,
+            'weekStart': settings.week_start
+        }
+
+    @staticmethod
+    def _to_object(json_settings):
+        return Settings(
+            auto_add_hangouts=json_settings.get('autoAddHangouts', False),
+            date_field_order=json_settings.get('dateFieldOrder', 'MDY'),
+            default_event_length=json_settings.get('defaultEventLength', 60),
+            format24_hour_time=json_settings.get('format24HourTime', False),
+            hide_invitations=json_settings.get('hideInvitations', False),
+            hide_weekends=json_settings.get('hideWeekends', False),
+            locale=json_settings.get('locale', 'en'),
+            remind_on_responded_events_only=json_settings.get('remindOnRespondedEventsOnly', False),
+            show_declined_events=json_settings.get('showDeclinedEvents', True),
+            timezone=json_settings.get('timezone', 'Etc/GMT'),
+            use_keyboard_shortcuts=json_settings.get('useKeyboardShortcuts', True),
+            week_start=json_settings.get('weekStart', 0)
+        )
diff --git a/gcsa/settings.py b/gcsa/settings.py
new file mode 100644
index 00000000..e2a7233d
--- /dev/null
+++ b/gcsa/settings.py
@@ -0,0 +1,77 @@
+class Settings:
+    """Represents settings that users can change from the Calendar UI, such as the user's time zone.
+    They can be retrieved via :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_settings`."""
+
+    def __init__(
+            self,
+            *,
+            auto_add_hangouts: bool = False,
+            date_field_order: str = 'MDY',
+            default_event_length: int = 60,
+            format24_hour_time: bool = False,
+            hide_invitations: bool = False,
+            hide_weekends: bool = False,
+            locale: str = 'en',
+            remind_on_responded_events_only: bool = False,
+            show_declined_events: bool = True,
+            timezone: str = 'Etc/GMT',
+            use_keyboard_shortcuts: bool = True,
+            week_start: int = 0
+    ):
+        """
+            :param auto_add_hangouts:
+                    Whether to automatically add Hangouts to all events.
+            :param date_field_order:
+                    What should the order of day (D), month (M) and year (Y) be when displaying dates.
+            :param default_event_length:
+                    The default length of events (in minutes) that were created without an explicit duration.
+            :param format24_hour_time:
+                    Whether to show the time in 24-hour format.
+            :param hide_invitations:
+                    Whether to hide events to which the user is invited but hasn't acted on (for example by responding).
+            :param hide_weekends:
+                    Whether the weekends should be hidden when displaying a week.
+            :param locale:
+                    User's locale.
+            :param remind_on_responded_events_only:
+                    Whether event reminders should be sent only for events with the user's response status "Yes" and
+                    "Maybe".
+            :param show_declined_events:
+                    Whether events to which the user responded "No" should be shown on the user's calendar.
+            :param timezone:
+                    The ID of the user's timezone.
+            :param use_keyboard_shortcuts:
+                    Whether the keyboard shortcuts are enabled.
+            :param week_start:
+                    Whether the week should start on Sunday (0), Monday (1) or Saturday (6).
+        """
+        self.auto_add_hangouts = auto_add_hangouts
+        self.date_field_order = date_field_order
+        self.default_event_length = default_event_length
+        self.format24_hour_time = format24_hour_time
+        self.hide_invitations = hide_invitations
+        self.hide_weekends = hide_weekends
+        self.locale = locale
+        self.remind_on_responded_events_only = remind_on_responded_events_only
+        self.show_declined_events = show_declined_events
+        self.timezone = timezone
+        self.use_keyboard_shortcuts = use_keyboard_shortcuts
+        self.week_start = week_start
+
+    def __str__(self):
+        return f'User settings:\n' \
+               f'auto_add_hangouts={self.auto_add_hangouts}\n' \
+               f'date_field_order={self.date_field_order}\n' \
+               f'default_event_length={self.default_event_length}\n' \
+               f'format24_hour_time={self.format24_hour_time}\n' \
+               f'hide_invitations={self.hide_invitations}\n' \
+               f'hide_weekends={self.hide_weekends}\n' \
+               f'locale={self.locale}\n' \
+               f'remind_on_responded_events_only={self.remind_on_responded_events_only}\n' \
+               f'show_declined_events={self.show_declined_events}\n' \
+               f'timezone={self.timezone}\n' \
+               f'use_keyboard_shortcuts={self.use_keyboard_shortcuts}\n' \
+               f'week_start={self.week_start}'
+
+    def __repr__(self):
+        return self.__str__()
diff --git a/gcsa/util/date_time_util.py b/gcsa/util/date_time_util.py
index 3a0d1053..5d8b8901 100644
--- a/gcsa/util/date_time_util.py
+++ b/gcsa/util/date_time_util.py
@@ -1,10 +1,10 @@
 from datetime import datetime, date
 
 import pytz
-from tzlocal import get_localzone
+from tzlocal import get_localzone_name
 
 
-def insure_localisation(dt, timezone=str(get_localzone())):
+def ensure_localisation(dt, timezone=get_localzone_name()):
     """Insures localisation with provided timezone on "datetime" object.
 
     Does nothing to object of type "date"."""
diff --git a/setup.py b/setup.py
index cea7ae05..71862bf3 100755
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@ def run(self):
 
 here = os.path.abspath(os.path.dirname(__file__))
 
-VERSION = '1.3.0'
+VERSION = '2.0.0'
 
 
 class UploadCommand(Command):
@@ -61,6 +61,23 @@ def run(self):
 with open('README.rst') as f:
     long_description = ''.join(f.readlines())
 
+DOCS_REQUIRES = [
+    'sphinx',
+    'sphinx-rtd-theme',
+]
+
+TEST_REQUIRES = [
+    'setuptools',
+    'pytest',
+    'pytest-pep8',
+    'pytest-cov',
+    'pyfakefs',
+    'flake8',
+    'pep8-naming',
+    'twine',
+    'tox'
+]
+
 setup(
     name='gcsa',
     version=VERSION,
@@ -75,26 +92,21 @@ def run(self):
     zip_safe=False,
     packages=find_packages(exclude=("tests", "tests.*")),
     install_requires=[
-        "tzlocal>=2,<3",
+        "tzlocal>=4,<5",
+        "pytz",
         "google-api-python-client>=1.8",
         "google-auth-httplib2>=0.0.4",
-        "google-auth-oauthlib>=0.4,<0.5",
+        "google-auth-oauthlib>=0.5,<1.0",
         "python-dateutil>=2.7",
         "beautiful_date>=2.0.0",
     ],
     extras_require={
         'dev': [
-            'setuptools',
-            'pytest',
-            'pytest-pep8',
-            'pytest-cov',
-            'pyfakefs',
-            'sphinx<2',
-            'sphinx-rtd-theme<0.5',
-            'flake8',
-            'twine',
-            'tox'
-        ]
+            *TEST_REQUIRES,
+            *DOCS_REQUIRES
+        ],
+        'tests': TEST_REQUIRES,
+        'docs': DOCS_REQUIRES
     },
     classifiers=[
         'License :: OSI Approved :: MIT License',
@@ -106,6 +118,7 @@ def run(self):
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
     ],
     cmdclass={
         'upload': UploadCommand,
diff --git a/tox.ini b/tox.ini
index 2267d35d..db37b6f1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,10 +6,14 @@ python =
     3.6: pytest
     3.7: pytest
     3.8: pytest
-    3.9: pytest, flake8, sphinx
+    3.9: pytest
+    3.10: pytest, flake8, sphinx
 
 [flake8]
 max-line-length = 120
+per-file-ignores =
+    # naming conventions broken by googleapiclient
+    tests/google_calendar_tests/mock_services/*: N802,N803
 
 [coverage:report]
 exclude_lines =
@@ -41,6 +45,7 @@ commands =
 [testenv:flake8]
 deps =
     flake8
+    pep8-naming
 commands =
     flake8 gcsa tests setup.py
 


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index bd430bcb..fc2eac97 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,9 +12,9 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [ 3.6, 3.7, 3.8, 3.9 ]
+        python-version: [ '3.6', '3.7', '3.8', '3.9', '3.10' ]
         include:
-          - python-version: 3.9
+          - python-version: '3.10'
             note: with-style-and-docs-checks
 
     steps:
diff --git a/tests/google_calendar_tests/__init__.py b/tests/google_calendar_tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/google_calendar_tests/mock_services/__init__.py b/tests/google_calendar_tests/mock_services/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/google_calendar_tests/mock_services/mock_calendar_list_requests.py b/tests/google_calendar_tests/mock_services/mock_calendar_list_requests.py
new file mode 100644
index 00000000..4f7a8edf
--- /dev/null
+++ b/tests/google_calendar_tests/mock_services/mock_calendar_list_requests.py
@@ -0,0 +1,78 @@
+from gcsa.calendar import CalendarListEntry
+from gcsa.serializers.calendar_serializer import CalendarListEntrySerializer
+from .util import executable
+
+
+class MockCalendarListRequests:
+    """Emulates GoogleCalendar.service.calendarList()"""
+    CALENDAR_LIST_ENTRIES_PER_PAGE = 3
+
+    def __init__(self):
+        self.test_calendars = [
+            CalendarListEntry(
+                summary_override=f'Summery override {i}',
+                _summary=f'Secondary {i}',
+                calendar_id=str(i)
+            )
+            for i in range(7)
+        ]
+        self.test_calendars.append(
+            CalendarListEntry(
+                summary_override='Primary',
+                _summary='Primary',
+                calendar_id='primary'
+            )
+        )
+
+    @property
+    def test_calendars_by_id(self):
+        return {c.id: c for c in self.test_calendars}
+
+    @executable
+    def list(self, pageToken, **_):
+        page = pageToken or 0  # page number in this case
+        page_calendars = self.test_calendars[
+                         page * self.CALENDAR_LIST_ENTRIES_PER_PAGE:(page + 1) * self.CALENDAR_LIST_ENTRIES_PER_PAGE
+                         ]
+        next_page = page + 1 if (page + 1) * self.CALENDAR_LIST_ENTRIES_PER_PAGE < len(self.test_calendars) else None
+
+        return {
+            'items': [
+                CalendarListEntrySerializer.to_json(c)
+                for c in page_calendars
+            ],
+            'nextPageToken': next_page
+        }
+
+    @executable
+    def get(self, calendarId):
+        """Emulates GoogleCalendar.service.calendarList().get().execute()"""
+        try:
+            return CalendarListEntrySerializer.to_json(self.test_calendars_by_id[calendarId])
+        except KeyError:
+            # shouldn't get here in tests
+            raise ValueError(f'Calendar with id {calendarId} does not exist')
+
+    @executable
+    def insert(self, body, colorRgbFormat):
+        """Emulates GoogleCalendar.service.calendarList().insert().execute()"""
+        calendar = CalendarListEntrySerializer.to_object(body)
+        self.test_calendars.append(calendar)
+        return CalendarListEntrySerializer.to_json(calendar)
+
+    @executable
+    def update(self, calendarId, body, colorRgbFormat):
+        """Emulates GoogleCalendar.service.calendars().insert().execute()"""
+        calendar = CalendarListEntrySerializer.to_object(body)
+        for i in range(len(self.test_calendars)):
+            if calendarId == self.test_calendars[i].id:
+                self.test_calendars[i] = calendar
+                return CalendarListEntrySerializer.to_json(calendar)
+
+        # shouldn't get here in tests
+        raise ValueError(f'Calendar with id {calendarId} does not exist')
+
+    @executable
+    def delete(self, calendarId):
+        """Emulates GoogleCalendar.service.calendars().delete().execute()"""
+        self.test_calendars = [c for c in self.test_calendars if c.id != calendarId]
diff --git a/tests/google_calendar_tests/mock_services/mock_calendars_requests.py b/tests/google_calendar_tests/mock_services/mock_calendars_requests.py
new file mode 100644
index 00000000..ea658846
--- /dev/null
+++ b/tests/google_calendar_tests/mock_services/mock_calendars_requests.py
@@ -0,0 +1,81 @@
+from gcsa.calendar import Calendar, AccessRoles
+from gcsa.serializers.calendar_serializer import CalendarSerializer
+from .util import executable
+
+
+class MockCalendarsRequests:
+    """Emulates GoogleCalendar.service.calendars()"""
+
+    def __init__(self):
+        self.test_calendars = [
+            Calendar(
+                summary=f'Secondary {i}',
+                calendar_id=str(i),
+                description=f'Description {i}',
+                location=f'Location {i}',
+                timezone=f'Timezone {i}',
+                allowed_conference_solution_types=[
+                    AccessRoles.FREE_BUSY_READER,
+                    AccessRoles.READER
+                ]
+            )
+            for i in range(7)
+        ]
+        self.test_calendars.append(
+            Calendar(
+                summary='Primary',
+                calendar_id='primary',
+                description='Description',
+                location='Location',
+                timezone='Timezone',
+                allowed_conference_solution_types=[
+                    AccessRoles.FREE_BUSY_READER,
+                    AccessRoles.READER,
+                    AccessRoles.WRITER,
+                    AccessRoles.OWNER,
+                ]
+            )
+        )
+
+    @property
+    def test_calendars_by_id(self):
+        return {c.id: c for c in self.test_calendars}
+
+    @executable
+    def get(self, calendarId):
+        """Emulates GoogleCalendar.service.calendars().get().execute()"""
+        try:
+            return CalendarSerializer.to_json(self.test_calendars_by_id[calendarId])
+        except KeyError:
+            # shouldn't get here in tests
+            raise ValueError(f'Calendar with id {calendarId} does not exist')
+
+    @executable
+    def insert(self, body):
+        """Emulates GoogleCalendar.service.calendars().insert().execute()"""
+        calendar = CalendarSerializer.to_object(body)
+        calendar.calendar_id = str(len(self.test_calendars))
+        self.test_calendars.append(calendar)
+        return CalendarSerializer.to_json(calendar)
+
+    @executable
+    def update(self, calendarId, body):
+        """Emulates GoogleCalendar.service.calendars().insert().execute()"""
+        calendar = CalendarSerializer.to_object(body)
+        for i in range(len(self.test_calendars)):
+            if calendarId == self.test_calendars[i].id:
+                self.test_calendars[i] = calendar
+                return CalendarSerializer.to_json(calendar)
+
+        # shouldn't get here in tests
+        raise ValueError(f'Calendar with id {calendarId} does not exist')
+
+    @executable
+    def delete(self, calendarId):
+        """Emulates GoogleCalendar.service.calendars().delete().execute()"""
+        self.test_calendars = [c for c in self.test_calendars if c.id != calendarId]
+
+    @executable
+    def clear(self, calendarId):
+        """Emulates GoogleCalendar.service.calendars().clear().execute()"""
+        pass
diff --git a/tests/google_calendar_tests/mock_services/mock_colors_requests.py b/tests/google_calendar_tests/mock_services/mock_colors_requests.py
new file mode 100644
index 00000000..a3689c78
--- /dev/null
+++ b/tests/google_calendar_tests/mock_services/mock_colors_requests.py
@@ -0,0 +1,27 @@
+from .util import executable
+
+
+class MockColorsRequests:
+    """Emulates GoogleCalendar.service.colors()"""
+
+    def __init__(self):
+        self.test_colors = {
+            'event': {
+                '1': {'background': '#a4bdfc', 'foreground': '#1d1d1d'},
+                '2': {'background': '#7ae7bf', 'foreground': '#1d1d1d'},
+                '3': {'background': '#dbadff', 'foreground': '#1d1d1d'},
+                '4': {'background': '#ff887c', 'foreground': '#1d1d1d'},
+            },
+            'calendar': {
+                '1': {'background': '#ac725e', 'foreground': '#1d1d1d'},
+                '2': {'background': '#d06b64', 'foreground': '#1d1d1d'},
+                '3': {'background': '#f83a22', 'foreground': '#1d1d1d'},
+                '4': {'background': '#fa573c', 'foreground': '#1d1d1d'},
+                '5': {'background': '#fc573c', 'foreground': '#1d1d1d'},
+            }
+        }
+
+    @executable
+    def get(self, **_):
+        """Emulates GoogleCalendar.service.colors().get().execute()"""
+        return self.test_colors
diff --git a/tests/google_calendar_tests/mock_services/mock_events_requests.py b/tests/google_calendar_tests/mock_services/mock_events_requests.py
new file mode 100644
index 00000000..24aee63c
--- /dev/null
+++ b/tests/google_calendar_tests/mock_services/mock_events_requests.py
@@ -0,0 +1,202 @@
+from .util import executable
+
+import dateutil
+from beautiful_date import D, days, years, hours
+
+from gcsa.attendee import Attendee
+from gcsa.event import Event
+from gcsa.serializers.event_serializer import EventSerializer
+from gcsa.util.date_time_util import ensure_localisation
+
+
+class MockEventsRequests:
+    """Emulates GoogleCalendar.service.events()"""
+
+    EVENTS_PER_PAGE = 3
+
+    def __init__(self):
+        self.test_events = [
+            Event(
+                'test{}'.format(i),
+                start=ensure_localisation(D.today()[:] + i * days + i * hours),
+                event_id=f'event_id_{str(i)}',
+                _updated=ensure_localisation(D.today()[:] + (i + 1) * days + i * hours),
+                attendees=[
+                    Attendee(email='{}@gmail.com'.format(attendee_name.lower()), display_name=attendee_name)
+                ] if attendee_name else None
+            )
+            for i, attendee_name in zip(range(1, 10), ['John', 'Josh'] + [''] * 8)
+        ]
+
+    @property
+    def test_events_by_id(self):
+        return {e.id: e for e in self.test_events}
+
+    @executable
+    def instances(self, eventId, **_):
+        """Emulates GoogleCalendar.service.events().instances().execute()"""
+
+        if eventId == 'event_id_1':
+            recurring_instances = [
+                Event(
+                    'Recurring event 1',
+                    start=D.today()[:] + 1 * days,
+                    event_id='event_id_1_' + (D.today()[:] + (i + 1) * days).isoformat() + 'Z',
+                    _updated=D.today()[:] + 5 * days,
+                    _recurring_event_id='event_id_1',
+
+                ) for i in range(1, 10)
+            ]
+        elif eventId == 'event_id_2':
+            recurring_instances = [
+                Event(
+                    'Recurring event 2',
+                    start=D.today()[:] + 2 * days,
+                    event_id='event_id_2_' + (D.today()[:] + (i + 2) * days).isoformat() + 'Z',
+                    _updated=D.today()[:] + 5 * days,
+                    _recurring_event_id='event_id_2',
+
+                ) for i in range(1, 5)
+            ]
+        else:
+            # shouldn't get here in tests
+            raise ValueError(f'Event with id {eventId} does not exist')
+
+        return {
+            'items': recurring_instances,
+            'nextPageToken': None
+        }
+
+    @executable
+    def list(self, timeMin, timeMax, orderBy, singleEvents, pageToken, q, **_):
+        """Emulates GoogleCalendar.service.events().list().execute()"""
+
+        time_min = dateutil.parser.parse(timeMin)
+        time_max = dateutil.parser.parse(timeMax)
+        page = pageToken or 0  # page number in this case
+
+        test_events = self.test_events.copy()
+
+        recurring_event = Event('Recurring event',
+                                start=ensure_localisation(D.today()[:] + 2 * days),
+                                event_id='recurring_id',
+                                _updated=ensure_localisation(D.today()[:] + 3 * days))
+        recurring_instances = [
+            Event(
+                recurring_event.summary,
+                start=recurring_event.start + i * days,
+                event_id=recurring_event.id + '_' + (recurring_event.start + i * days).isoformat() + 'Z',
+                _updated=recurring_event.updated,
+                _recurring_event_id=recurring_event.id,
+
+            ) for i in range(10)
+        ]
+
+        if singleEvents:
+            test_events.extend(recurring_instances)
+        else:
+            test_events.append(recurring_event)
+
+        event_in_a_year = Event(
+            'test42',
+            start=ensure_localisation(D.today()[:] + 1 * years + 2 * days),
+            event_id='42',
+            _updated=ensure_localisation(D.today()[:] + 1 * years + 3 * days),
+            attendees=[
+                Attendee(email='frank@gmail.com', display_name='Frank')
+            ]
+        )
+        test_events.append(event_in_a_year)
+
+        def _filter(e):
+            return (
+                    (time_min <= e.start and e.end < time_max) and
+                    (
+                            not q or
+                            q in e.summary or
+                            (e.description and q in e.description) or
+                            (e.attendees and any((a.display_name and q in a.display_name) for a in e.attendees))
+                    )
+            )
+
+        def _sort_key(e):
+            if orderBy is None:
+                return e.id
+            if orderBy == 'startTime':
+                return e.start
+            if orderBy == 'updated':
+                return e.updated
+
+        filtered_events = list(filter(_filter, test_events))
+        ordered_events = sorted(filtered_events, key=_sort_key)
+        serialized_events = list(map(EventSerializer.to_json, ordered_events))
+
+        current_page_events = ordered_events[page * self.EVENTS_PER_PAGE:(page + 1) * self.EVENTS_PER_PAGE]
+        next_page = page + 1 if (page + 1) * self.EVENTS_PER_PAGE < len(serialized_events) else None
+        return {
+            'items': current_page_events,
+            'nextPageToken': next_page
+        }
+
+    @executable
+    def get(self, eventId, **_):
+        """Emulates GoogleCalendar.service.events().get().execute()"""
+        try:
+            return EventSerializer.to_json(self.test_events_by_id[eventId])
+        except KeyError:
+            # shouldn't get here in tests
+            raise ValueError(f'Event with id {eventId} does not exist')
+
+    @executable
+    def insert(self, body, **_):
+        """Emulates GoogleCalendar.service.events().insert().execute()"""
+        event = EventSerializer.to_object(body)
+
+        if event.id is None:
+            event.event_id = f'event_id_{len(self.test_events) + 1}'
+        else:
+            assert event.id not in self.test_events_by_id
+
+        self.test_events.append(event)
+        return EventSerializer.to_json(event)
+
+    @executable
+    def quickAdd(self, text, **_):
+        """Emulates GoogleCalendar.service.events().quickAdd().execute()"""
+        summary, start = text.split(' at ')
+        event = Event(
+            summary,
+            start=dateutil.parser.parse(start)
+        )
+
+        event.event_id = f'event_id_{len(self.test_events) + 1}'
+        self.test_events.append(event)
+        return EventSerializer.to_json(event)
+
+    @executable
+    def update(self, eventId, body, **_):
+        """Emulates GoogleCalendar.service.events().update().execute()"""
+
+        updated_event = EventSerializer.to_object(body)
+        for i in range(len(self.test_events)):
+            if eventId == self.test_events[i].id:
+                self.test_events[i] = updated_event
+                return EventSerializer.to_json(updated_event)
+
+        # shouldn't get here in tests
+        raise ValueError(f'Event with id {eventId} does not exist')
+
+    @executable
+    def import_(self, body, **_):
+        """Emulates GoogleCalendar.service.events().import_().execute()"""
+        return self.insert(body).execute()
+
+    @executable
+    def move(self, eventId, destination, **_):
+        """Emulates GoogleCalendar.service.events().move().execute()"""
+        return self.get(eventId=eventId).execute()
+
+    @executable
+    def delete(self, eventId, **_):
+        """Emulates GoogleCalendar.service.events().delete().execute()"""
+        self.test_events = [e for e in self.test_events if e.id != eventId]
diff --git a/tests/google_calendar_tests/mock_services/mock_service.py b/tests/google_calendar_tests/mock_services/mock_service.py
new file mode 100644
index 00000000..322e76e0
--- /dev/null
+++ b/tests/google_calendar_tests/mock_services/mock_service.py
@@ -0,0 +1,31 @@
+from .mock_calendar_list_requests import MockCalendarListRequests
+from .mock_calendars_requests import MockCalendarsRequests
+from .mock_colors_requests import MockColorsRequests
+from .mock_events_requests import MockEventsRequests
+from .mock_settings_requests import MockSettingsRequests
+
+
+class MockService:
+    """Emulates GoogleCalendar.service"""
+
+    def __init__(self):
+        self._events = MockEventsRequests()
+        self._calendars = MockCalendarsRequests()
+        self._calendar_list = MockCalendarListRequests()
+        self._colors = MockColorsRequests()
+        self._settings = MockSettingsRequests()
+
+    def events(self):
+        return self._events
+
+    def calendars(self):
+        return self._calendars
+
+    def calendarList(self):
+        return self._calendar_list
+
+    def colors(self):
+        return self._colors
+
+    def settings(self):
+        return self._settings
diff --git a/tests/google_calendar_tests/mock_services/mock_settings_requests.py b/tests/google_calendar_tests/mock_services/mock_settings_requests.py
new file mode 100644
index 00000000..bcf6819b
--- /dev/null
+++ b/tests/google_calendar_tests/mock_services/mock_settings_requests.py
@@ -0,0 +1,26 @@
+from .util import executable
+
+
+class MockSettingsRequests:
+    """Emulates GoogleCalendar.service.settings()"""
+
+    @executable
+    def list(self, **_):
+        """Emulates GoogleCalendar.service.settings().list().execute()"""
+        return {
+            "nextPageToken": None,
+            "items": [
+                {'id': 'autoAddHangouts', 'value': True},
+                {'id': 'dateFieldOrder', 'value': 'DMY'},
+                {'id': 'defaultEventLength', 'value': 45},
+                {'id': 'format24HourTime', 'value': True},
+                {'id': 'hideInvitations', 'value': True},
+                {'id': 'hideWeekends', 'value': True},
+                {'id': 'locale', 'value': 'cz'},
+                {'id': 'remindOnRespondedEventsOnly', 'value': True},
+                {'id': 'showDeclinedEvents', 'value': False},
+                {'id': 'timezone', 'value': 'Europe/Prague'},
+                {'id': 'useKeyboardShortcuts', 'value': False},
+                {'id': 'weekStart', 'value': 1}
+            ]
+        }
diff --git a/tests/google_calendar_tests/mock_services/util.py b/tests/google_calendar_tests/mock_services/util.py
new file mode 100644
index 00000000..61b1d5fe
--- /dev/null
+++ b/tests/google_calendar_tests/mock_services/util.py
@@ -0,0 +1,27 @@
+class MockToken:
+    def __init__(self, valid, refresh_token='refresh_token'):
+        self.valid = valid
+        self.expired = not valid
+        self.refresh_token = refresh_token
+
+    def refresh(self, _):
+        self.valid = True
+        self.expired = False
+
+
+def executable(fn):
+    """Decorator that stores data received from the function in object that returns that data when
+    called its `execute` method. Emulates HttpRequest from googleapiclient."""
+
+    class Executable:
+        def __init__(self, data):
+            self.data = data
+
+        def execute(self):
+            return self.data
+
+    def wrapper(*args, **kwargs):
+        data = fn(*args, **kwargs)
+        return Executable(data)
+
+    return wrapper
diff --git a/tests/google_calendar_tests/test_authentication.py b/tests/google_calendar_tests/test_authentication.py
new file mode 100644
index 00000000..9e868de3
--- /dev/null
+++ b/tests/google_calendar_tests/test_authentication.py
@@ -0,0 +1,86 @@
+import pickle
+from os import path
+from unittest.mock import patch
+
+from pyfakefs.fake_filesystem_unittest import TestCase
+
+from gcsa.google_calendar import GoogleCalendar
+from tests.google_calendar_tests.mock_services.util import MockToken
+
+
+class TestGoogleCalendarCredentials(TestCase):
+
+    def setUp(self):
+        self.setUpPyfakefs()
+
+        self.credentials_dir = '/.credentials'
+        self.credentials_path = path.join(self.credentials_dir, 'credentials.json')
+        self.fs.create_dir(self.credentials_dir)
+        self.fs.create_file(self.credentials_path)
+
+        self.valid_token_path = path.join(self.credentials_dir, 'valid_token.pickle')
+        self.expired_token_path = path.join(self.credentials_dir, 'expired_token.pickle')
+
+        with open(self.valid_token_path, 'wb') as token_file:
+            pickle.dump(MockToken(valid=True), token_file)
+        with open(self.expired_token_path, 'wb') as token_file:
+            pickle.dump(MockToken(valid=False), token_file)
+
+        self._add_mocks()
+
+    def _add_mocks(self):
+        self.build_patcher = patch('googleapiclient.discovery.build', return_value=None).start()
+
+        class MockAuthFlow:
+            def run_local_server(self, *args, **kwargs):
+                return MockToken(valid=True)
+
+        self.from_client_secrets_file_patcher = patch(
+            'google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file',
+            return_value=MockAuthFlow()
+        ).start()
+
+    def tearDown(self):
+        self.build_patcher.stop()
+        self.from_client_secrets_file_patcher.stop()
+
+    def test_with_given_credentials(self):
+        GoogleCalendar(credentials=MockToken(valid=True))
+        self.assertFalse(self.from_client_secrets_file_patcher.called)
+
+    def test_with_given_credentials_expired(self):
+        gc = GoogleCalendar(credentials=MockToken(valid=False))
+        self.assertTrue(gc.credentials.valid)
+        self.assertFalse(gc.credentials.expired)
+
+    def test_get_default_credentials_path_exist(self):
+        self.fs.create_dir(path.join(path.expanduser('~'), '.credentials'))
+        self.assertEqual(
+            path.join(path.expanduser('~'), '.credentials/credentials.json'),
+            GoogleCalendar._get_default_credentials_path()
+        )
+
+    def test_get_default_credentials_path_not_exist(self):
+        self.assertFalse(path.exists(path.join(path.expanduser('~'), '.credentials')))
+        self.assertEqual(
+            path.join(path.expanduser('~'), '.credentials/credentials.json'),
+            GoogleCalendar._get_default_credentials_path()
+        )
+        self.assertTrue(path.exists(path.join(path.expanduser('~'), '.credentials')))
+
+    def test_get_token_valid(self):
+        gc = GoogleCalendar(token_path=self.valid_token_path)
+        self.assertTrue(gc.credentials.valid)
+        self.assertFalse(self.from_client_secrets_file_patcher.called)
+
+    def test_get_token_expired(self):
+        gc = GoogleCalendar(token_path=self.expired_token_path)
+        self.assertTrue(gc.credentials.valid)
+        self.assertFalse(gc.credentials.expired)
+        self.assertFalse(self.from_client_secrets_file_patcher.called)
+
+    def test_get_token_invalid_refresh(self):
+        gc = GoogleCalendar(credentials_path=self.credentials_path)
+        self.assertTrue(gc.credentials.valid)
+        self.assertFalse(gc.credentials.expired)
+        self.assertTrue(self.from_client_secrets_file_patcher.called)
diff --git a/tests/google_calendar_tests/test_calendar_list_service.py b/tests/google_calendar_tests/test_calendar_list_service.py
new file mode 100644
index 00000000..95dac0a2
--- /dev/null
+++ b/tests/google_calendar_tests/test_calendar_list_service.py
@@ -0,0 +1,67 @@
+from gcsa.calendar import CalendarListEntry, Calendar
+from tests.google_calendar_tests.test_case_with_mocked_service import TestCaseWithMockedService
+
+
+class TestEventsService(TestCaseWithMockedService):
+    def test_get_calendar_list(self):
+        calendars = list(self.gc.get_calendar_list())
+        self.assertEqual(len(calendars), 8)
+        self.assertTrue(any(c.id == 'primary' for c in calendars))
+
+    def test_get_calendar_list_entry(self):
+        calendar = self.gc.get_calendar_list_entry()
+        self.assertEqual(calendar.id, 'primary')
+        self.assertIsInstance(calendar, CalendarListEntry)
+
+        calendar = self.gc.get_calendar('1')
+        self.assertEqual(calendar.id, '1')
+
+    def test_add_calendar_list_entry(self):
+        calendar = CalendarListEntry(
+            calendar_id='test_calendar_list_entry',
+            _summary='Summary',
+            summary_override='Summary override'
+        )
+        new_calendar = self.gc.add_calendar_list_entry(calendar)
+
+        self.assertIsNotNone(new_calendar.id)
+        self.assertEqual(calendar.summary_override, new_calendar.summary_override)
+
+    def test_update_calendar_list_entry(self):
+        calendar = CalendarListEntry(
+            calendar_id='test_calendar_list_entry',
+            _summary='Summary',
+            summary_override='Summary override'
+        )
+        new_calendar = self.gc.add_calendar_list_entry(calendar)
+
+        self.assertEqual(calendar.summary_override, new_calendar.summary_override)
+
+        new_calendar.summary_override = 'Updated summary override'
+        updated_calendar = self.gc.update_calendar_list_entry(new_calendar)
+
+        self.assertEqual(new_calendar.summary_override, updated_calendar.summary_override)
+
+        retrieved_updated_calendar = self.gc.get_calendar_list_entry(new_calendar.id)
+        self.assertEqual(retrieved_updated_calendar.summary_override, updated_calendar.summary_override)
+
+    def test_delete_calendar_list_entry(self):
+        calendar = Calendar(
+            summary='Summary'
+        )
+        with self.assertRaises(ValueError):
+            # no calendar_id
+            self.gc.delete_calendar_list_entry(calendar)
+
+        calendar = CalendarListEntry(
+            calendar_id='test_calendar_list_entry',
+            _summary='Summary',
+            summary_override='Summary override'
+        )
+
+        self.gc.delete_calendar_list_entry(calendar)
+        self.gc.delete_calendar_list_entry('2')
+
+        with self.assertRaises(TypeError):
+            # should be a Calendar, CalendarListEntry or calendar id as a string
+            self.gc.delete_calendar_list_entry(calendar=None)
diff --git a/tests/google_calendar_tests/test_calendars_service.py b/tests/google_calendar_tests/test_calendars_service.py
new file mode 100644
index 00000000..32a9c20b
--- /dev/null
+++ b/tests/google_calendar_tests/test_calendars_service.py
@@ -0,0 +1,72 @@
+from gcsa.calendar import Calendar, AccessRoles
+from tests.google_calendar_tests.test_case_with_mocked_service import TestCaseWithMockedService
+
+
+class TestEventsService(TestCaseWithMockedService):
+    def test_get_calendar(self):
+        calendar = self.gc.get_calendar()
+        self.assertEqual(calendar.id, 'primary')
+        self.assertIsInstance(calendar, Calendar)
+
+        calendar = self.gc.get_calendar('1')
+        self.assertEqual(calendar.id, '1')
+
+    def test_add_calendar(self):
+        calendar = Calendar(
+            summary='secondary',
+            description='Description secondary',
+            location='Location secondary',
+            timezone='Timezone secondary',
+            allowed_conference_solution_types=[
+                AccessRoles.FREE_BUSY_READER,
+                AccessRoles.READER
+            ]
+        )
+        new_calendar = self.gc.add_calendar(calendar)
+
+        self.assertIsNotNone(new_calendar.id)
+        self.assertEqual(calendar.summary, new_calendar.summary)
+
+    def test_update_calendar(self):
+        calendar = Calendar(
+            summary='secondary',
+            description='Description secondary',
+            location='Location secondary',
+            timezone='Timezone secondary',
+            allowed_conference_solution_types=[
+                AccessRoles.FREE_BUSY_READER,
+                AccessRoles.READER
+            ]
+        )
+        new_calendar = self.gc.add_calendar(calendar)
+
+        self.assertEqual(calendar.summary, new_calendar.summary)
+
+        new_calendar.summary = 'Updated summary'
+        updated_calendar = self.gc.update_calendar(new_calendar)
+
+        self.assertEqual(new_calendar.summary, updated_calendar.summary)
+
+        retrieved_updated_calendar = self.gc.get_calendar(new_calendar.id)
+        self.assertEqual(retrieved_updated_calendar.summary, updated_calendar.summary)
+
+    def test_delete_calendar(self):
+        calendar = Calendar(
+            summary='secondary'
+        )
+
+        with self.assertRaises(ValueError):
+            # no calendar_id
+            self.gc.delete_calendar(calendar)
+
+        new_calendar = self.gc.add_calendar(calendar)
+        self.gc.delete_calendar(new_calendar)
+        self.gc.delete_calendar('2')
+
+        with self.assertRaises(TypeError):
+            # should be a Calendar or calendar id as a string
+            self.gc.delete_calendar(None)
+
+    def test_clear_calendar(self):
+        self.gc.clear_calendar()
+        self.gc.clear()
diff --git a/tests/google_calendar_tests/test_case_with_mocked_service.py b/tests/google_calendar_tests/test_case_with_mocked_service.py
new file mode 100644
index 00000000..75e1cb8f
--- /dev/null
+++ b/tests/google_calendar_tests/test_case_with_mocked_service.py
@@ -0,0 +1,18 @@
+from unittest.mock import patch
+
+from pyfakefs.fake_filesystem_unittest import TestCase
+
+from gcsa.google_calendar import GoogleCalendar
+from tests.google_calendar_tests.mock_services.mock_service import MockService
+from tests.google_calendar_tests.mock_services.util import MockToken
+
+
+class TestCaseWithMockedService(TestCase):
+    def setUp(self):
+        self.build_patcher = patch('googleapiclient.discovery.build', return_value=MockService())
+        self.build_patcher.start()
+
+        self.gc = GoogleCalendar(credentials=MockToken(valid=True))
+
+    def tearDown(self):
+        self.build_patcher.stop()
diff --git a/tests/google_calendar_tests/test_colors_service.py b/tests/google_calendar_tests/test_colors_service.py
new file mode 100644
index 00000000..f075cd0b
--- /dev/null
+++ b/tests/google_calendar_tests/test_colors_service.py
@@ -0,0 +1,11 @@
+from tests.google_calendar_tests.test_case_with_mocked_service import TestCaseWithMockedService
+
+
+class TestColorsService(TestCaseWithMockedService):
+    def test_list_event_colors(self):
+        event_colors = self.gc.list_event_colors()
+        self.assertEqual(len(event_colors), 4)
+
+    def test_list_calendar_colors(self):
+        calendar_colors = self.gc.list_calendar_colors()
+        self.assertEqual(len(calendar_colors), 5)
diff --git a/tests/google_calendar_tests/test_events_service.py b/tests/google_calendar_tests/test_events_service.py
new file mode 100644
index 00000000..6ee975ee
--- /dev/null
+++ b/tests/google_calendar_tests/test_events_service.py
@@ -0,0 +1,231 @@
+from beautiful_date import D, days, years, hours
+
+from gcsa.event import Event
+from gcsa.util.date_time_util import ensure_localisation
+from tests.google_calendar_tests.test_case_with_mocked_service import TestCaseWithMockedService
+
+
+class TestEventsService(TestCaseWithMockedService):
+
+    def test_get_events_default(self):
+        events = list(self.gc.get_events())
+        self.assertEqual(len(events), 10)
+        self.assertFalse(any(e.is_recurring_instance for e in events))
+
+        events = list(self.gc)
+        self.assertEqual(len(events), 10)
+        self.assertFalse(any(e.is_recurring_instance for e in events))
+
+    def test_get_events_time_limits(self):
+        time_min = ensure_localisation(D.today()[:] + 5 * days)
+        events = list(self.gc.get_events(time_min=time_min))
+        self.assertEqual(len(events), 6)
+        self.assertTrue(all(e.start >= time_min for e in events))
+
+        time_min = ensure_localisation(D.today()[:] + 5 * days)
+        events = list(self.gc[time_min])
+        self.assertEqual(len(events), 6)
+        self.assertTrue(all(e.start >= time_min for e in events))
+
+        time_max = ensure_localisation(D.today()[:] + 1 * years + 7 * days)
+        events = list(self.gc.get_events(time_max=time_max))
+        self.assertEqual(len(events), 11)
+        self.assertTrue(all(e.end < time_max for e in events))
+
+        time_max = ensure_localisation(D.today()[:] + 7 * days)
+        events = list(self.gc.get_events(time_max=time_max))
+        self.assertEqual(len(events), 7)
+        self.assertTrue(all(e.end < time_max for e in events))
+
+        events = list(self.gc.get_events(time_min=time_min, time_max=time_max))
+        self.assertEqual(len(events), 2)
+        self.assertTrue(all(time_min <= e.start and e.end < time_max for e in events))
+
+        events = list(self.gc[time_min:time_max])
+        self.assertEqual(len(events), 2)
+        self.assertTrue(all(time_min <= e.start and e.end < time_max for e in events))
+
+        time_min = D.today() + 5 * days
+        time_max = D.today() + 7 * days
+        events = list(self.gc.get_events(time_min=time_min, time_max=time_max))
+        self.assertEqual(len(events), 3)
+
+        time_min = ensure_localisation(time_min[0:0])
+        time_max = ensure_localisation(time_max[23:59:59])
+        self.assertTrue(all(time_min <= e.start and e.end < time_max for e in events))
+
+        with self.assertRaises(NotImplementedError):
+            _ = self.gc[5]
+        with self.assertRaises(ValueError):
+            _ = self.gc[5:10]
+
+    def test_get_events_single_events(self):
+        events = list(self.gc.get_events(single_events=True))
+        self.assertEqual(len(events), 19)
+        self.assertTrue(all(e.is_recurring_instance for e in events if e.summary == 'Recurring event'))
+
+        events = list(self.gc.get_events(single_events=False))
+        self.assertEqual(len(events), 10)
+        self.assertTrue(all(not e.is_recurring_instance for e in events if e.summary == 'Recurring event'))
+
+        with self.assertRaises(ValueError):
+            # can only be used with single events
+            list(self.gc.get_events(order_by='startTime'))
+
+    def test_get_events_order_by(self):
+        events = list(self.gc.get_events(order_by='updated'))
+        self.assertEqual(len(events), 10)
+        self.assertEqual(events[0].id, min(events, key=lambda e: e.updated).id)
+        self.assertEqual(events[-1].id, max(events, key=lambda e: e.updated).id)
+
+        events = list(self.gc[::'updated'])
+        self.assertEqual(len(events), 10)
+        self.assertEqual(events[0].id, min(events, key=lambda e: e.updated).id)
+        self.assertEqual(events[-1].id, max(events, key=lambda e: e.updated).id)
+
+        events = list(self.gc[::'startTime'])
+        self.assertEqual(len(events), 19)
+        self.assertEqual(events[0].id, min(events, key=lambda e: e.start).id)
+        self.assertEqual(events[-1].id, max(events, key=lambda e: e.start).id)
+
+        events = list(self.gc.get_events(order_by='startTime', single_events=True))
+        self.assertEqual(len(events), 19)
+        self.assertEqual(events[0].id, min(events, key=lambda e: e.start).id)
+        self.assertEqual(events[-1].id, max(events, key=lambda e: e.start).id)
+
+    def test_get_events_query(self):
+        events = list(self.gc.get_events(query='test4', time_max=D.today()[:] + 2 * years))
+        self.assertEqual(len(events), 2)  # test4 and test42
+
+        events = list(self.gc.get_events(query='Jo', time_max=D.today()[:] + 2 * years))
+        self.assertEqual(len(events), 2)  # with John and Josh
+
+        events = list(self.gc.get_events(query='Josh', time_max=D.today()[:] + 2 * years))
+        self.assertEqual(len(events), 1)
+
+        events = list(self.gc.get_events(query='Frank', time_max=D.today()[:] + 2 * years))
+        self.assertEqual(len(events), 1)
+
+    def test_get_recurring_instances(self):
+        events = list(self.gc.get_instances(recurring_event='event_id_1'))
+        self.assertEqual(len(events), 9)
+        self.assertTrue(all(e.id.startswith('event_id_1') for e in events))
+
+        recurring_event = Event(
+            'recurring event',
+            D.today()[:],
+            event_id='event_id_2'
+        )
+        events = list(self.gc.get_instances(recurring_event=recurring_event))
+        self.assertEqual(len(events), 4)
+        self.assertTrue(all(e.id.startswith('event_id_2') for e in events))
+
+    def test_get_event(self):
+        start = D.today()[:]
+        end = start + 2 * hours
+        event = Event(
+            'test_event',
+            start=start,
+            end=end
+        )
+        new_event = self.gc.add_event(event)
+
+        received_new_event = self.gc.get_event(new_event.id)
+        self.assertEqual(received_new_event, new_event)
+
+    def test_add_event(self):
+        start = D.today()[:]
+        end = start + 2 * hours
+        event = Event(
+            'test_event',
+            start=start,
+            end=end
+        )
+        new_event = self.gc.add_event(event)
+
+        self.assertIsNotNone(new_event.id)
+
+        received_new_event = self.gc.get_event(new_event.id)
+        self.assertEqual(received_new_event, new_event)
+
+    def test_add_quick_event(self):
+        start = ensure_localisation(D.today()[:])
+        summary = 'Breakfast'
+        event_string = f'{summary} at {start.isoformat()}'
+
+        new_event = self.gc.add_quick_event(event_string)
+
+        self.assertIsNotNone(new_event.id)
+        self.assertEqual(new_event.summary, summary)
+        self.assertEqual(new_event.start, start)
+
+        received_new_event = self.gc.get_event(new_event.id)
+        self.assertEqual(received_new_event, new_event)
+
+    def test_update_event(self):
+        start = ensure_localisation(D.today()[:])
+        summary = 'test_event'
+        event = Event(
+            summary,
+            start=start
+        )
+        new_event = self.gc.add_event(event)
+        self.assertEqual(new_event.summary, summary)
+
+        new_summary = 'test_event_updated'
+        new_start = start + 1 * days
+
+        new_event.summary = new_summary
+        new_event.start = new_start
+
+        updated_event = self.gc.update_event(new_event)
+        self.assertEqual(updated_event, new_event)
+
+        received_updated_event = self.gc.get_event(new_event.id)
+        self.assertEqual(received_updated_event, new_event)
+
+    def test_import_event(self):
+        start = D.today()[:]
+        end = start + 2 * hours
+        event = Event(
+            'test_event',
+            start=start,
+            end=end,
+            event_id='test_event'
+        )
+        new_event = self.gc.import_event(event)
+        received_new_event = self.gc.get_event(new_event.id)
+        self.assertEqual(received_new_event, new_event)
+
+    def test_move_event(self):
+        start = D.today()[:]
+        end = start + 2 * hours
+        event = Event(
+            'test_event',
+            start=start,
+            end=end,
+            event_id='test_event_id'
+        )
+        new_event = self.gc.add_event(event)
+        received_new_event = self.gc.move_event(new_event, destination_calendar_id='test_dest_calendar')
+        self.assertEqual(received_new_event, new_event)
+
+    def test_delete_event(self):
+        start = D.today()[:]
+        end = start + 2 * hours
+        event = Event(
+            'test_event',
+            start=start,
+            end=end
+        )
+        with self.assertRaises(ValueError):
+            # no event_id
+            self.gc.delete_event(event)
+
+        new_event = self.gc.add_event(event)
+        self.gc.delete_event(new_event)
+        self.gc.delete_event('test_event_id')
+
+        with self.assertRaises(TypeError):
+            # should be event or event id as a string
+            self.gc.delete_event(start)
diff --git a/tests/google_calendar_tests/test_settings_service.py b/tests/google_calendar_tests/test_settings_service.py
new file mode 100644
index 00000000..af435373
--- /dev/null
+++ b/tests/google_calendar_tests/test_settings_service.py
@@ -0,0 +1,18 @@
+from tests.google_calendar_tests.test_case_with_mocked_service import TestCaseWithMockedService
+
+
+class TestSettingsService(TestCaseWithMockedService):
+    def test_get_settings(self):
+        settings = self.gc.get_settings()
+        self.assertTrue(settings.auto_add_hangouts)
+        self.assertEqual(settings.date_field_order, 'DMY')
+        self.assertEqual(settings.default_event_length, 45)
+        self.assertTrue(settings.format24_hour_time)
+        self.assertTrue(settings.hide_invitations)
+        self.assertTrue(settings.hide_weekends)
+        self.assertEqual(settings.locale, 'cz')
+        self.assertTrue(settings.remind_on_responded_events_only)
+        self.assertFalse(settings.show_declined_events)
+        self.assertEqual(settings.timezone, 'Europe/Prague')
+        self.assertFalse(settings.use_keyboard_shortcuts)
+        self.assertEqual(settings.week_start, 1)
diff --git a/tests/test_base_serializer.py b/tests/test_base_serializer.py
index caeb00d4..a25d9231 100644
--- a/tests/test_base_serializer.py
+++ b/tests/test_base_serializer.py
@@ -4,7 +4,7 @@
 
 
 class TestBaseSerializer(TestCase):
-    def test_assure_dict(self):
+    def test_ensure_dict(self):
         json_str = """
         {
             "key": "value",
@@ -19,19 +19,13 @@ def test_assure_dict(self):
 
         json_object = (1, 2, 3)  # non-json object
 
-        self.assertDictEqual(BaseSerializer.assure_dict(json_str), json_dict)
-        self.assertDictEqual(BaseSerializer.assure_dict(json_dict), json_dict)
+        self.assertDictEqual(BaseSerializer.ensure_dict(json_str), json_dict)
+        self.assertDictEqual(BaseSerializer.ensure_dict(json_dict), json_dict)
 
         with self.assertRaises(TypeError):
-            BaseSerializer.assure_dict(json_object)
+            BaseSerializer.ensure_dict(json_object)
 
     def test_subclass(self):
-        # __init_subclass__ was introduced in Python 3.6
-        # In older versions tests are not executed
-        import sys
-        if sys.version_info[1] < 6:
-            return
-
         class Apple:
             pass
 
diff --git a/tests/test_calendar.py b/tests/test_calendar.py
new file mode 100644
index 00000000..959c7809
--- /dev/null
+++ b/tests/test_calendar.py
@@ -0,0 +1,373 @@
+from unittest import TestCase
+
+from gcsa.calendar import Calendar, CalendarListEntry, NotificationType, AccessRoles
+from gcsa.conference import SolutionType
+from gcsa.reminders import EmailReminder, PopupReminder
+from gcsa.serializers.calendar_serializer import CalendarSerializer, CalendarListEntrySerializer
+
+TEST_TIMEZONE = 'Pacific/Fiji'
+TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES = [SolutionType.HANGOUT, SolutionType.NAMED_HANGOUT]
+TEST_NOTIFICATION_TYPES = [NotificationType.EVENT_CREATION, NotificationType.EVENT_CHANGE]
+TEST_ACCESS_ROLE = AccessRoles.OWNER
+
+
+class TestCalendar(TestCase):
+    def test_init(self):
+        c = Calendar(
+            summary='Summary',
+            calendar_id='Calendar id',
+            description='Description',
+            location='Fiji',
+            timezone=TEST_TIMEZONE,
+            allowed_conference_solution_types=TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES
+        )
+
+        self.assertEqual(c.summary, 'Summary')
+        self.assertEqual(c.calendar_id, 'Calendar id')
+        self.assertEqual(c.id, 'Calendar id')
+        self.assertEqual(c.description, 'Description')
+        self.assertEqual(c.location, 'Fiji')
+        self.assertEqual(c.timezone, TEST_TIMEZONE)
+        self.assertListEqual(c.allowed_conference_solution_types, TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES)
+
+    def test_to_calendar_list_entry(self):
+        c = Calendar(
+            summary='Summary',
+            calendar_id='Calendar id',
+            description='Description',
+            location='Fiji',
+            timezone=TEST_TIMEZONE,
+            allowed_conference_solution_types=TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES
+        )
+
+        cle = c.to_calendar_list_entry(
+            summary_override='Summary override',
+            color_id='1',
+            background_color='#123123',
+            foreground_color='#234234',
+            hidden=False,
+            selected=True,
+            default_reminders=[EmailReminder(60), PopupReminder(15)],
+            notification_types=TEST_NOTIFICATION_TYPES
+        )
+        self.assertIsInstance(cle, CalendarListEntry)
+
+        self.assertEqual(cle.summary_override, 'Summary override')
+        self.assertEqual(cle.color_id, '1')
+        self.assertEqual(cle.background_color, '#123123')
+        self.assertEqual(cle.foreground_color, '#234234')
+        self.assertFalse(cle.hidden)
+        self.assertTrue(cle.selected)
+        self.assertEqual(cle.default_reminders, [EmailReminder(60), PopupReminder(15)])
+        self.assertEqual(cle.notification_types, TEST_NOTIFICATION_TYPES)
+
+        c_without_id = Calendar(
+            summary='Summary',
+        )
+        with self.assertRaises(ValueError):
+            c_without_id.to_calendar_list_entry()
+
+    def test_repr_str(self):
+        c = Calendar(
+            summary='Summary',
+            calendar_id='Calendar id',
+            description='Description'
+        )
+        self.assertEqual(str(c), 'Summary - Description')
+        self.assertEqual(repr(c), '<Calendar Summary - Description>')
+
+    def test_eq(self):
+        c1 = Calendar(
+            summary='Summary',
+            calendar_id='Calendar id',
+            description='Description',
+            location='Fiji',
+            timezone=TEST_TIMEZONE,
+            allowed_conference_solution_types=TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES
+        )
+        c2 = Calendar(
+            summary='Summary2',
+            calendar_id='Calendar id2',
+            description='Description2',
+            location='Fiji',
+            timezone=TEST_TIMEZONE,
+            allowed_conference_solution_types=TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES
+        )
+        self.assertEqual(c1, c1)
+        self.assertNotEqual(c1, c2)
+        self.assertNotEqual(c1, 'Calendar')
+
+
+class TestCalendarSerializer(TestCase):
+
+    def test_to_json(self):
+        c = Calendar(
+            summary='Summary',
+            calendar_id='Calendar id',
+            description='Description',
+            location='Fiji',
+            timezone=TEST_TIMEZONE,
+            allowed_conference_solution_types=TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES
+        )
+        expected_calendar_json = {
+            "id": 'Calendar id',
+            "summary": 'Summary',
+            "description": 'Description',
+            "location": 'Fiji',
+            "timeZone": TEST_TIMEZONE,
+            "conferenceProperties": {
+                "allowedConferenceSolutionTypes": TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES
+            }
+        }
+        self.assertDictEqual(CalendarSerializer.to_json(c), expected_calendar_json)
+
+        c = Calendar(
+            summary='Summary',
+            description='Description',
+            timezone=TEST_TIMEZONE
+        )
+        expected_calendar_json = {
+            "summary": 'Summary',
+            "description": 'Description',
+            "timeZone": TEST_TIMEZONE
+        }
+        self.assertDictEqual(CalendarSerializer.to_json(c), expected_calendar_json)
+
+    def test_to_object(self):
+        calendar_json = {
+            "id": 'Calendar id',
+            "summary": 'Summary',
+            "description": 'Description',
+            "location": 'Fiji',
+            "timeZone": TEST_TIMEZONE,
+            "conferenceProperties": {
+                "allowedConferenceSolutionTypes": TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES
+            }
+        }
+
+        serializer = CalendarSerializer(calendar_json)
+        c = serializer.get_object()
+
+        self.assertEqual(c.summary, 'Summary')
+        self.assertEqual(c.calendar_id, 'Calendar id')
+        self.assertEqual(c.description, 'Description')
+        self.assertEqual(c.location, 'Fiji')
+        self.assertEqual(c.timezone, TEST_TIMEZONE)
+        self.assertListEqual(c.allowed_conference_solution_types, TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES)
+
+        calendar_json = """{
+            "id": "Calendar id",
+            "summary": "Summary",
+            "location": "Fiji"
+        }"""
+
+        serializer = CalendarSerializer(calendar_json)
+        c = serializer.get_object()
+
+        self.assertEqual(c.summary, 'Summary')
+        self.assertEqual(c.calendar_id, 'Calendar id')
+        self.assertIsNone(c.description)
+        self.assertEqual(c.location, 'Fiji')
+        self.assertIsNone(c.timezone)
+        self.assertIsNone(c.allowed_conference_solution_types)
+
+
+class TestCalendarListEntry(TestCase):
+    def test_init(self):
+        c = CalendarListEntry(
+            summary_override='Summary override',
+            color_id='1',
+            background_color='#123123',
+            foreground_color='#234234',
+            hidden=False,
+            selected=True,
+            default_reminders=[EmailReminder(60), PopupReminder(15)],
+            notification_types=TEST_NOTIFICATION_TYPES,
+            _access_role=TEST_ACCESS_ROLE,
+            _primary=True,
+            _deleted=False,
+
+            _summary='Summary',
+            calendar_id='Calendar id',
+            _description='Description',
+            _location='Fiji',
+            _timezone=TEST_TIMEZONE,
+            _allowed_conference_solution_types=TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES
+        )
+
+        self.assertEqual(c.summary_override, 'Summary override')
+        self.assertEqual(c.color_id, '1')
+        self.assertEqual(c.background_color, '#123123')
+        self.assertEqual(c.foreground_color, '#234234')
+        self.assertFalse(c.hidden)
+        self.assertTrue(c.selected)
+        self.assertEqual(c.default_reminders, [EmailReminder(60), PopupReminder(15)])
+        self.assertEqual(c.notification_types, TEST_NOTIFICATION_TYPES)
+        self.assertEqual(c.access_role, TEST_ACCESS_ROLE)
+        self.assertTrue(c.primary)
+        self.assertFalse(c.deleted)
+
+        self.assertEqual(c.summary, 'Summary')
+        self.assertEqual(c.calendar_id, 'Calendar id')
+        self.assertEqual(c.description, 'Description')
+        self.assertEqual(c.location, 'Fiji')
+        self.assertEqual(c.timezone, TEST_TIMEZONE)
+        self.assertListEqual(c.allowed_conference_solution_types, TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES)
+
+    def test_repr_str(self):
+        c = CalendarListEntry(
+            calendar_id='Calendar id',
+            summary_override='Summary override',
+            _summary='Summary',
+        )
+        self.assertEqual(str(c), 'Summary override - (Summary)')
+        self.assertEqual(repr(c), '<CalendarListEntry Summary override - (Summary)>')
+
+    def test_eq(self):
+        c1 = CalendarListEntry(
+            calendar_id='Calendar id',
+            summary_override='Summary override',
+            _summary='Summary',
+        )
+        c2 = CalendarListEntry(
+            calendar_id='Calendar id2',
+            summary_override='Summary override2',
+            _summary='Summary2',
+        )
+        self.assertEqual(c1, c1)
+        self.assertNotEqual(c1, c2)
+        self.assertNotEqual(c1, 'Calendar')
+
+
+class TestCalendarListEntrySerializer(TestCase):
+
+    def test_to_json(self):
+        c = CalendarListEntry(
+            summary_override='Summary override',
+            color_id='1',
+            background_color='#123123',
+            foreground_color='#234234',
+            hidden=False,
+            selected=True,
+            default_reminders=[EmailReminder(60), PopupReminder(15)],
+            notification_types=TEST_NOTIFICATION_TYPES,
+            _access_role=TEST_ACCESS_ROLE,
+            _primary=True,
+            _deleted=False,
+
+            _summary='Summary',
+            calendar_id='Calendar id',
+            _description='Description',
+            _location='Fiji',
+            _timezone=TEST_TIMEZONE,
+            _allowed_conference_solution_types=TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES
+        )
+        expected_calendar_json = {
+            'id': 'Calendar id',
+            'summaryOverride': 'Summary override',
+            'colorId': '1',
+            'backgroundColor': '#123123',
+            'foregroundColor': '#234234',
+            'hidden': False,
+            'selected': True,
+            'defaultReminders': [
+                {'method': 'email', 'minutes': 60},
+                {'method': 'popup', 'minutes': 15}
+            ],
+            'notificationSettings': {
+                'notifications': [
+                    {'type': 'eventCreation', 'method': 'email'},
+                    {'type': 'eventChange', 'method': 'email'}
+                ]
+            }
+        }
+        self.assertDictEqual(CalendarListEntrySerializer.to_json(c), expected_calendar_json)
+
+        c = CalendarListEntry(
+            summary_override='Summary override',
+            calendar_id='Calendar id',
+            _timezone=TEST_TIMEZONE,
+        )
+        expected_calendar_json = {
+            'id': 'Calendar id',
+            'summaryOverride': 'Summary override',
+            'hidden': False,
+            'selected': False,
+        }
+        self.assertDictEqual(CalendarListEntrySerializer.to_json(c), expected_calendar_json)
+
+    def test_to_object(self):
+        calendar_json = {
+            "id": 'Calendar id',
+            "summary": 'Summary',
+            "description": 'Description',
+            "location": 'Fiji',
+            "timeZone": TEST_TIMEZONE,
+            "summaryOverride": 'Summary override',
+            "colorId": '1',
+            "backgroundColor": '#123123',
+            "foregroundColor": '#234234',
+            "hidden": False,
+            "selected": True,
+            "accessRole": TEST_ACCESS_ROLE,
+            "defaultReminders": [
+                {"method": 'email', "minutes": 60},
+                {"method": 'popup', "minutes": 15}
+            ],
+            "notificationSettings": {
+                "notifications": [
+                    {'type': 'eventCreation', 'method': 'email'},
+                    {'type': 'eventChange', 'method': 'email'}
+                ]
+            },
+            "primary": True,
+            "deleted": False,
+            "conferenceProperties": {
+                "allowedConferenceSolutionTypes": TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES
+            }
+        }
+
+        serializer = CalendarListEntrySerializer(calendar_json)
+        c = serializer.get_object()
+
+        self.assertEqual(c.summary_override, 'Summary override')
+        self.assertEqual(c.color_id, '1')
+        self.assertEqual(c.background_color, '#123123')
+        self.assertEqual(c.foreground_color, '#234234')
+        self.assertFalse(c.hidden)
+        self.assertTrue(c.selected)
+        self.assertListEqual(c.default_reminders, [EmailReminder(60), PopupReminder(15)])
+        self.assertEqual(c.notification_types, TEST_NOTIFICATION_TYPES)
+        self.assertEqual(c.access_role, TEST_ACCESS_ROLE)
+        self.assertTrue(c.primary)
+        self.assertFalse(c.deleted)
+
+        self.assertEqual(c.summary, 'Summary')
+        self.assertEqual(c.calendar_id, 'Calendar id')
+        self.assertEqual(c.description, 'Description')
+        self.assertEqual(c.location, 'Fiji')
+        self.assertEqual(c.timezone, TEST_TIMEZONE)
+        self.assertListEqual(c.allowed_conference_solution_types, TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES)
+
+        calendar_json = """{
+            "id": "Calendar id",
+            "foregroundColor": "#234234",
+            "defaultReminders": [
+                {"method": "email", "minutes": 60},
+                {"method": "popup", "minutes": 15}
+            ],
+            "primary": true,
+            "conferenceProperties": {
+                "allowedConferenceSolutionTypes": ["eventHangout", "eventNamedHangout"]
+            }
+        }"""
+
+        serializer = CalendarListEntrySerializer(calendar_json)
+        c = serializer.get_object()
+
+        self.assertEqual(c.foreground_color, '#234234')
+        self.assertListEqual(c.default_reminders, [EmailReminder(60), PopupReminder(15)])
+        self.assertTrue(c.primary)
+
+        self.assertEqual(c.calendar_id, 'Calendar id')
+        self.assertListEqual(c.allowed_conference_solution_types, TEST_ALLOWED_CONFERENCE_SOLUTION_TYPES)
diff --git a/tests/test_event.py b/tests/test_event.py
index be09532b..1480faca 100644
--- a/tests/test_event.py
+++ b/tests/test_event.py
@@ -8,7 +8,7 @@
 from gcsa.recurrence import Recurrence, DAILY, SU, SA, MONDAY, WEEKLY
 from gcsa.reminders import PopupReminder, EmailReminder
 from gcsa.serializers.event_serializer import EventSerializer
-from gcsa.util.date_time_util import insure_localisation
+from gcsa.util.date_time_util import ensure_localisation
 
 TEST_TIMEZONE = 'Pacific/Fiji'
 
@@ -20,8 +20,8 @@ def test_init(self):
             event_id='123',
             start=(1 / Feb / 2019)[9:00],
             end=(31 / Dec / 2019)[23:59],
-            _created=insure_localisation((20 / Nov / 2020)[16:19], TEST_TIMEZONE),
-            _updated=insure_localisation((25 / Nov / 2020)[16:19], TEST_TIMEZONE),
+            _created=ensure_localisation((20 / Nov / 2020)[16:19], TEST_TIMEZONE),
+            _updated=ensure_localisation((25 / Nov / 2020)[16:19], TEST_TIMEZONE),
             timezone=TEST_TIMEZONE,
             description='Everyday breakfast',
             location='Home',
@@ -43,10 +43,10 @@ def test_init(self):
 
         self.assertEqual(event.summary, 'Breakfast')
         self.assertEqual(event.id, '123')
-        self.assertEqual(event.start, insure_localisation((1 / Feb / 2019)[9:00], TEST_TIMEZONE))
-        self.assertEqual(event.end, insure_localisation((31 / Dec / 2019)[23:59], TEST_TIMEZONE))
-        self.assertEqual(event.created, insure_localisation((20 / Nov / 2020)[16:19], TEST_TIMEZONE))
-        self.assertEqual(event.updated, insure_localisation((25 / Nov / 2020)[16:19], TEST_TIMEZONE))
+        self.assertEqual(event.start, ensure_localisation((1 / Feb / 2019)[9:00], TEST_TIMEZONE))
+        self.assertEqual(event.end, ensure_localisation((31 / Dec / 2019)[23:59], TEST_TIMEZONE))
+        self.assertEqual(event.created, ensure_localisation((20 / Nov / 2020)[16:19], TEST_TIMEZONE))
+        self.assertEqual(event.updated, ensure_localisation((25 / Nov / 2020)[16:19], TEST_TIMEZONE))
         self.assertEqual(event.description, 'Everyday breakfast')
         self.assertEqual(event.location, 'Home')
         self.assertEqual(len(event.recurrence), 3)
@@ -62,7 +62,7 @@ def test_init_no_end(self):
         event = Event('Good day', start, timezone=TEST_TIMEZONE)
         self.assertEqual(event.end, start + 1 * days)
 
-        start = insure_localisation((1 / Jul / 2019)[12:00], TEST_TIMEZONE)
+        start = ensure_localisation((1 / Jul / 2019)[12:00], TEST_TIMEZONE)
         event = Event('Lunch', start, timezone=TEST_TIMEZONE)
         self.assertEqual(event.end, start + 1 * hours)
 
@@ -160,7 +160,7 @@ def test_equal(self):
             description='Everyday breakfast',
             location='Home',
             recurrence=Recurrence.rule(freq=DAILY),
-            color_id=1,
+            color_id='1',
             visibility=Visibility.PRIVATE,
             attendees='mail@gmail.com',
             attachments=Attachment(title='My doc', **attachments_dp),
@@ -183,8 +183,8 @@ def test_equal(self):
         self.assertNotEqual(Event(**dp, recurrence=Recurrence.rule(freq=DAILY)),
                             Event(**dp, recurrence=Recurrence.rule(freq=WEEKLY)))
 
-        self.assertNotEqual(Event(**dp, color_id=1),
-                            Event(**dp, color_id=2))
+        self.assertNotEqual(Event(**dp, color_id='1'),
+                            Event(**dp, color_id='2'))
 
         self.assertNotEqual(Event(**dp, visibility=Visibility.PRIVATE),
                             Event(**dp, visibility=Visibility.PUBLIC))
@@ -490,7 +490,7 @@ def test_to_json_updated(self):
             'Good day',
             start=(1 / Jul / 2020)[11:22:33],
             timezone=TEST_TIMEZONE,
-            _updated=insure_localisation((25 / Nov / 2020)[11:22:33], timezone=TEST_TIMEZONE)
+            _updated=ensure_localisation((25 / Nov / 2020)[11:22:33], timezone=TEST_TIMEZONE)
         )
         expected_event_json = {
             'summary': 'Good day',
@@ -585,10 +585,10 @@ def test_to_object(self):
         event = serializer.get_object()
 
         self.assertEqual(event.summary, 'Good day')
-        self.assertEqual(event.start, insure_localisation((1 / Jan / 2019)[11:22:33], TEST_TIMEZONE))
-        self.assertEqual(event.end, insure_localisation((1 / Jan / 2019)[12:22:33], TEST_TIMEZONE))
-        self.assertEqual(event.updated, insure_localisation((25 / Nov / 2020)[14:53:46], 'UTC'))
-        self.assertEqual(event.created, insure_localisation((24 / Nov / 2020)[14:53:46], 'UTC'))
+        self.assertEqual(event.start, ensure_localisation((1 / Jan / 2019)[11:22:33], TEST_TIMEZONE))
+        self.assertEqual(event.end, ensure_localisation((1 / Jan / 2019)[12:22:33], TEST_TIMEZONE))
+        self.assertEqual(event.updated, ensure_localisation((25 / Nov / 2020)[14:53:46], 'UTC'))
+        self.assertEqual(event.created, ensure_localisation((24 / Nov / 2020)[14:53:46], 'UTC'))
         self.assertEqual(event.description, 'Very good day indeed')
         self.assertEqual(event.location, 'Prague')
         self.assertEqual(len(event.recurrence), 3)
diff --git a/tests/test_google_calendar.py b/tests/test_google_calendar.py
deleted file mode 100644
index b93a76e5..00000000
--- a/tests/test_google_calendar.py
+++ /dev/null
@@ -1,381 +0,0 @@
-import pickle
-from os import path
-from unittest.mock import patch
-
-import dateutil
-from beautiful_date import D, days, years, hours
-from pyfakefs.fake_filesystem_unittest import TestCase
-
-from gcsa.attendee import Attendee
-from gcsa.event import Event
-from gcsa.google_calendar import GoogleCalendar
-from gcsa.serializers.event_serializer import EventSerializer
-from gcsa.util.date_time_util import insure_localisation
-
-
-class MockToken:
-    def __init__(self, valid, refresh_token='refresh_token'):
-        self.valid = valid
-        self.expired = not valid
-        self.refresh_token = refresh_token
-
-    def refresh(self, _):
-        self.valid = True
-        self.expired = False
-
-
-class TestGoogleCalendarCredentials(TestCase):
-
-    def setUp(self):
-        self.setUpPyfakefs()
-
-        self.credentials_dir = '/.credentials'
-        self.credentials_path = path.join(self.credentials_dir, 'credentials.json')
-        self.fs.create_dir(self.credentials_dir)
-        self.fs.create_file(self.credentials_path)
-
-        self.valid_token_path = path.join(self.credentials_dir, 'valid_token.pickle')
-        self.expired_token_path = path.join(self.credentials_dir, 'expired_token.pickle')
-
-        with open(self.valid_token_path, 'wb') as token_file:
-            pickle.dump(MockToken(valid=True), token_file)
-        with open(self.expired_token_path, 'wb') as token_file:
-            pickle.dump(MockToken(valid=False), token_file)
-
-        self._add_mocks()
-
-    def _add_mocks(self):
-        self.build_patcher = patch('googleapiclient.discovery.build', return_value=None).start()
-
-        class MockAuthFlow:
-            def run_local_server(self, *args, **kwargs):
-                return MockToken(valid=True)
-
-        self.from_client_secrets_file_patcher = patch(
-            'google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file',
-            return_value=MockAuthFlow()
-        ).start()
-
-    def tearDown(self):
-        self.build_patcher.stop()
-        self.from_client_secrets_file_patcher.stop()
-
-    def test_with_given_credentials(self):
-        GoogleCalendar(credentials=MockToken(valid=True))
-        self.assertFalse(self.from_client_secrets_file_patcher.called)
-
-    def test_with_given_credentials_expired(self):
-        gc = GoogleCalendar(credentials=MockToken(valid=False))
-        self.assertTrue(gc.credentials.valid)
-        self.assertFalse(gc.credentials.expired)
-
-    def test_get_default_credentials_path_exist(self):
-        self.fs.create_dir(path.join(path.expanduser('~'), '.credentials'))
-        self.assertEqual(
-            path.join(path.expanduser('~'), '.credentials/credentials.json'),
-            GoogleCalendar._get_default_credentials_path()
-        )
-
-    def test_get_default_credentials_path_not_exist(self):
-        self.assertFalse(path.exists(path.join(path.expanduser('~'), '.credentials')))
-        self.assertEqual(
-            path.join(path.expanduser('~'), '.credentials/credentials.json'),
-            GoogleCalendar._get_default_credentials_path()
-        )
-        self.assertTrue(path.exists(path.join(path.expanduser('~'), '.credentials')))
-
-    def test_get_token_valid(self):
-        gc = GoogleCalendar(token_path=self.valid_token_path)
-        self.assertTrue(gc.credentials.valid)
-        self.assertFalse(self.from_client_secrets_file_patcher.called)
-
-    def test_get_token_expired(self):
-        gc = GoogleCalendar(token_path=self.expired_token_path)
-        self.assertTrue(gc.credentials.valid)
-        self.assertFalse(gc.credentials.expired)
-        self.assertFalse(self.from_client_secrets_file_patcher.called)
-
-    def test_get_token_invalid_refresh(self):
-        gc = GoogleCalendar(credentials_path=self.credentials_path)
-        self.assertTrue(gc.credentials.valid)
-        self.assertFalse(gc.credentials.expired)
-        self.assertTrue(self.from_client_secrets_file_patcher.called)
-
-
-def executable(fn):
-    """Decorator that stores data received from the function in object that returns that data when
-    called its `execute` method. Emulates HttpRequest from googleapiclient."""
-
-    class Executable:
-        def __init__(self, data):
-            self.data = data
-
-        def execute(self):
-            return self.data
-
-    def wrapper(*args, **kwargs):
-        data = fn(*args, **kwargs)
-        return Executable(data)
-
-    return wrapper
-
-
-class MockEventsRequests:
-    """Emulates GoogleCalendar.service.events()"""
-
-    EVENTS_PER_PAGE = 3
-
-    @executable
-    def instances(self, **kwargs):
-        event_id = kwargs.pop('eventId')
-
-        if event_id == 'event_id_1':
-            recurring_instances = [
-                Event(
-                    'Recurring event 1',
-                    start=D.today()[:] + 1 * days,
-                    event_id='event_id_1_' + (D.today()[:] + (i + 1) * days).isoformat() + 'Z',
-                    _updated=D.today()[:] + 5 * days,
-                    _recurring_event_id='event_id_1',
-
-                ) for i in range(1, 10)
-            ]
-        elif event_id == 'event_id_2':
-            recurring_instances = [
-                Event(
-                    'Recurring event 2',
-                    start=D.today()[:] + 2 * days,
-                    event_id='event_id_2_' + (D.today()[:] + (i + 2) * days).isoformat() + 'Z',
-                    _updated=D.today()[:] + 5 * days,
-                    _recurring_event_id='event_id_2',
-
-                ) for i in range(1, 5)
-            ]
-        else:
-            # shouldn't get here in tests
-            raise ValueError
-
-        return {
-            'items': recurring_instances,
-            'nextPageToken': None
-        }
-
-    @executable
-    def list(self, **kwargs):
-        """Emulates GoogleCalendar.service.events().list().execute()"""
-
-        time_min = dateutil.parser.parse(kwargs['timeMin'])
-        time_max = dateutil.parser.parse(kwargs['timeMax'])
-        order_by = kwargs['orderBy']
-        single_events = kwargs['singleEvents']
-        page_token = kwargs['pageToken'] or 0  # page number in this case
-        q = kwargs['q']
-
-        test_events = [
-            Event(
-                'test{}'.format(i),
-                start=insure_localisation(D.today()[:] + i * days + i * hours),
-                event_id='1',
-                _updated=insure_localisation(D.today()[:] + (i + 1) * days + i * hours),
-                attendees=[
-                    Attendee(email='{}@gmail.com'.format(attendee_name.lower()), display_name=attendee_name)
-                ] if attendee_name else None
-            )
-            for i, attendee_name in zip(range(1, 10), ['John', 'Josh'] + [''] * 8)
-        ]
-
-        recurring_event = Event('Recurring event',
-                                start=insure_localisation(D.today()[:] + 2 * days),
-                                event_id='recurring_id',
-                                _updated=insure_localisation(D.today()[:] + 3 * days))
-        recurring_instances = [
-            Event(
-                recurring_event.summary,
-                start=recurring_event.start + i * days,
-                event_id=recurring_event.id + '_' + (recurring_event.start + i * days).isoformat() + 'Z',
-                _updated=recurring_event.updated,
-                _recurring_event_id=recurring_event.id,
-
-            ) for i in range(10)
-        ]
-
-        if single_events:
-            test_events.extend(recurring_instances)
-        else:
-            test_events.append(recurring_event)
-
-        event_in_a_year = Event(
-            'test42',
-            start=insure_localisation(D.today()[:] + 1 * years + 2 * days),
-            event_id='42',
-            _updated=insure_localisation(D.today()[:] + 1 * years + 3 * days),
-            attendees=[
-                Attendee(email='frank@gmail.com', display_name='Frank')
-            ]
-        )
-        test_events.append(event_in_a_year)
-
-        def _filter(e):
-            return (
-                    (time_min <= e.start and e.end < time_max) and
-                    (
-                            not q or
-                            q in e.summary or
-                            (e.description and q in e.description) or
-                            (e.attendees and any((a.display_name and q in a.display_name) for a in e.attendees))
-                    )
-            )
-
-        def _sort_key(e):
-            if order_by is None:
-                return e.id
-            if order_by == 'startTime':
-                return e.start
-            if order_by == 'updated':
-                return e.updated
-
-        filtered_events = list(filter(_filter, test_events))
-        ordered_events = sorted(filtered_events, key=_sort_key)
-        serialized_events = list(map(self._serialize_event, ordered_events))
-
-        current_page_events = ordered_events[page_token * self.EVENTS_PER_PAGE:(page_token + 1) * self.EVENTS_PER_PAGE]
-        return {
-            'items': current_page_events,
-            'nextPageToken': page_token + 1 if (page_token + 1) * 3 < len(serialized_events) else None
-        }
-
-    @staticmethod
-    def _serialize_event(e):
-        event_json = EventSerializer.to_json(e)
-        event_json['updated'] = e.updated.isoformat() + 'Z'
-        return event_json
-
-
-class MockService:
-    @staticmethod
-    def events():
-        return MockEventsRequests()
-
-
-class TestGoogleCalendarAPI(TestCase):
-    def setUp(self):
-        self.build_patcher = patch('googleapiclient.discovery.build', return_value=MockService())
-        self.build_patcher.start()
-
-        self.gc = GoogleCalendar(credentials=MockToken(valid=True))
-
-    def tearDown(self):
-        self.build_patcher.stop()
-
-    def test_get_events_default(self):
-        events = list(self.gc.get_events())
-        self.assertEqual(len(events), 10)
-        self.assertFalse(any(e.is_recurring_instance for e in events))
-
-        events = list(self.gc)
-        self.assertEqual(len(events), 10)
-        self.assertFalse(any(e.is_recurring_instance for e in events))
-
-    def test_get_events_time_limits(self):
-        time_min = insure_localisation(D.today()[:] + 5 * days)
-        events = list(self.gc.get_events(time_min=time_min))
-        self.assertEqual(len(events), 6)
-        self.assertTrue(all(e.start >= time_min for e in events))
-
-        time_min = insure_localisation(D.today()[:] + 5 * days)
-        events = list(self.gc[time_min])
-        self.assertEqual(len(events), 6)
-        self.assertTrue(all(e.start >= time_min for e in events))
-
-        time_max = insure_localisation(D.today()[:] + 1 * years + 7 * days)
-        events = list(self.gc.get_events(time_max=time_max))
-        self.assertEqual(len(events), 11)
-        self.assertTrue(all(e.end < time_max for e in events))
-
-        time_max = insure_localisation(D.today()[:] + 7 * days)
-        events = list(self.gc.get_events(time_max=time_max))
-        self.assertEqual(len(events), 7)
-        self.assertTrue(all(e.end < time_max for e in events))
-
-        events = list(self.gc.get_events(time_min=time_min, time_max=time_max))
-        self.assertEqual(len(events), 2)
-        self.assertTrue(all(time_min <= e.start and e.end < time_max for e in events))
-
-        events = list(self.gc[time_min:time_max])
-        self.assertEqual(len(events), 2)
-        self.assertTrue(all(time_min <= e.start and e.end < time_max for e in events))
-
-        time_min = D.today() + 5 * days
-        time_max = D.today() + 7 * days
-        events = list(self.gc.get_events(time_min=time_min, time_max=time_max))
-        self.assertEqual(len(events), 3)
-
-        time_min = insure_localisation(time_min[0:0])
-        time_max = insure_localisation(time_max[23:59:59])
-        self.assertTrue(all(time_min <= e.start and e.end < time_max for e in events))
-
-        with self.assertRaises(NotImplementedError):
-            _ = self.gc[5]
-        with self.assertRaises(ValueError):
-            _ = self.gc[5:10]
-
-    def test_get_events_single_events(self):
-        events = list(self.gc.get_events(single_events=True))
-        self.assertEqual(len(events), 19)
-        self.assertTrue(all(e.is_recurring_instance for e in events if e.summary == 'Recurring event'))
-
-        events = list(self.gc.get_events(single_events=False))
-        self.assertEqual(len(events), 10)
-        self.assertTrue(all(not e.is_recurring_instance for e in events if e.summary == 'Recurring event'))
-
-        with self.assertRaises(ValueError):
-            # can only be used with single events
-            list(self.gc.get_events(order_by='startTime'))
-
-    def test_get_events_order_by(self):
-        events = list(self.gc.get_events(order_by='updated'))
-        self.assertEqual(len(events), 10)
-        self.assertEqual(events[0].id, min(events, key=lambda e: e.updated).id)
-        self.assertEqual(events[-1].id, max(events, key=lambda e: e.updated).id)
-
-        events = list(self.gc[::'updated'])
-        self.assertEqual(len(events), 10)
-        self.assertEqual(events[0].id, min(events, key=lambda e: e.updated).id)
-        self.assertEqual(events[-1].id, max(events, key=lambda e: e.updated).id)
-
-        events = list(self.gc[::'startTime'])
-        self.assertEqual(len(events), 19)
-        self.assertEqual(events[0].id, min(events, key=lambda e: e.start).id)
-        self.assertEqual(events[-1].id, max(events, key=lambda e: e.start).id)
-
-        events = list(self.gc.get_events(order_by='startTime', single_events=True))
-        self.assertEqual(len(events), 19)
-        self.assertEqual(events[0].id, min(events, key=lambda e: e.start).id)
-        self.assertEqual(events[-1].id, max(events, key=lambda e: e.start).id)
-
-    def test_get_events_query(self):
-        events = list(self.gc.get_events(query='test4', time_max=D.today()[:] + 2 * years))
-        self.assertEqual(len(events), 2)  # test4 and test42
-
-        events = list(self.gc.get_events(query='Jo', time_max=D.today()[:] + 2 * years))
-        self.assertEqual(len(events), 2)  # with John and Josh
-
-        events = list(self.gc.get_events(query='Josh', time_max=D.today()[:] + 2 * years))
-        self.assertEqual(len(events), 1)
-
-        events = list(self.gc.get_events(query='Frank', time_max=D.today()[:] + 2 * years))
-        self.assertEqual(len(events), 1)
-
-    def test_get_recurring_instances(self):
-        events = list(self.gc.get_instances(recurring_event='event_id_1'))
-        self.assertEqual(len(events), 9)
-        self.assertTrue(all(e.id.startswith('event_id_1') for e in events))
-
-        recurring_event = Event(
-            'recurring event',
-            D.today()[:],
-            event_id='event_id_2'
-        )
-        events = list(self.gc.get_instances(recurring_event=recurring_event))
-        self.assertEqual(len(events), 4)
-        self.assertTrue(all(e.id.startswith('event_id_2') for e in events))
diff --git a/tests/test_settings.py b/tests/test_settings.py
new file mode 100644
index 00000000..421e9096
--- /dev/null
+++ b/tests/test_settings.py
@@ -0,0 +1,101 @@
+from unittest import TestCase
+
+from gcsa.serializers.settings_serializer import SettingsSerializer
+from gcsa.settings import Settings
+
+
+class TestSettings(TestCase):
+    def test_repr_str(self):
+        settings = Settings(
+            auto_add_hangouts=True,
+            date_field_order='DMY',
+            default_event_length=45,
+            format24_hour_time=True,
+            hide_invitations=True,
+            hide_weekends=True,
+            locale='cz',
+            remind_on_responded_events_only=True,
+            show_declined_events=False,
+            timezone='Europe/Prague',
+            use_keyboard_shortcuts=False,
+            week_start=1,
+        )
+        expected_str = \
+            "User settings:\n" \
+            "auto_add_hangouts=True\n" \
+            "date_field_order=DMY\n" \
+            "default_event_length=45\n" \
+            "format24_hour_time=True\n" \
+            "hide_invitations=True\n" \
+            "hide_weekends=True\n" \
+            "locale=cz\n" \
+            "remind_on_responded_events_only=True\n" \
+            "show_declined_events=False\n" \
+            "timezone=Europe/Prague\n" \
+            "use_keyboard_shortcuts=False\n" \
+            "week_start=1"
+        self.assertEqual(settings.__str__(), expected_str)
+        self.assertEqual(settings.__repr__(), expected_str)
+
+
+class TestSettingsSerializer(TestCase):
+    def test_to_json(self):
+        settings = Settings(
+            auto_add_hangouts=True,
+            date_field_order='DMY',
+            default_event_length=45,
+            format24_hour_time=True,
+            hide_invitations=True,
+            hide_weekends=True,
+            locale='cz',
+            remind_on_responded_events_only=True,
+            show_declined_events=False,
+            timezone='Europe/Prague',
+            use_keyboard_shortcuts=False,
+            week_start=1,
+        )
+        expected_json = {
+            'autoAddHangouts': settings.auto_add_hangouts,
+            'dateFieldOrder': settings.date_field_order,
+            'defaultEventLength': settings.default_event_length,
+            'format24HourTime': settings.format24_hour_time,
+            'hideInvitations': settings.hide_invitations,
+            'hideWeekends': settings.hide_weekends,
+            'locale': settings.locale,
+            'remindOnRespondedEventsOnly': settings.remind_on_responded_events_only,
+            'showDeclinedEvents': settings.show_declined_events,
+            'timezone': settings.timezone,
+            'useKeyboardShortcuts': settings.use_keyboard_shortcuts,
+            'weekStart': settings.week_start
+        }
+        self.assertDictEqual(SettingsSerializer(settings).get_json(), expected_json)
+
+    def test_to_object(self):
+        settings_json = {
+            'autoAddHangouts': True,
+            'dateFieldOrder': 'DMY',
+            'defaultEventLength': 45,
+            'format24HourTime': True,
+            'hideInvitations': True,
+            'hideWeekends': True,
+            'locale': 'cz',
+            'remindOnRespondedEventsOnly': True,
+            'showDeclinedEvents': False,
+            'timezone': 'Europe/Prague',
+            'useKeyboardShortcuts': False,
+            'weekStart': 1,
+        }
+        settings = SettingsSerializer(settings_json).get_object()
+
+        self.assertTrue(settings.auto_add_hangouts)
+        self.assertEqual(settings.date_field_order, 'DMY')
+        self.assertEqual(settings.default_event_length, 45)
+        self.assertTrue(settings.format24_hour_time)
+        self.assertTrue(settings.hide_invitations)
+        self.assertTrue(settings.hide_weekends)
+        self.assertEqual(settings.locale, 'cz')
+        self.assertTrue(settings.remind_on_responded_events_only)
+        self.assertFalse(settings.show_declined_events)
+        self.assertEqual(settings.timezone, 'Europe/Prague')
+        self.assertFalse(settings.use_keyboard_shortcuts)
+        self.assertEqual(settings.week_start, 1)
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 00000000..9f5cd4e2
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,25 @@
+from unittest import TestCase
+
+from beautiful_date import Sept
+
+from gcsa.util.date_time_util import ensure_localisation
+
+
+class TestReminder(TestCase):
+    def test_ensure_localisation(self):
+        initial_date = 23 / Sept / 2022
+        d = ensure_localisation(initial_date)
+        # Shouldn't do anything to date
+        self.assertEqual(initial_date, d)
+
+        initial_date_time = initial_date[:]
+        self.assertIsNone(initial_date_time.tzinfo)
+        dt_with_tz = ensure_localisation(initial_date_time)
+        self.assertIsNotNone(dt_with_tz.tzinfo)
+        self.assertNotEqual(dt_with_tz, initial_date_time)
+
+        dt_with_tz_unchanged = ensure_localisation(dt_with_tz)
+        self.assertEqual(dt_with_tz, dt_with_tz_unchanged)
+
+        with self.assertRaises(TypeError):
+            ensure_localisation('Hello')