Repository: python-gitlab/python-gitlab

Issue:
Feature Request : billable membership functionality
## Description of the problem, including code/CLI snippet

I can't thank you enough for making the library, its very very helpful. 

Would it be possible to add the functionality to get/remove the billable membership through this library as well?

## Expected Behavior

Functionality not available.

## Actual Behavior

Be able to get the billable members or remove any billable member to take away the seat

## Specifications

  - python-gitlab version: latest
  - API version you are using (v3/v4): v4
  - Gitlab server version (or gitlab.com): 



Discussion:

Resolving patch:
diff --git a/gitlab/v4/objects/groups.py b/gitlab/v4/objects/groups.py
--- a/gitlab/v4/objects/groups.py
+++ b/gitlab/v4/objects/groups.py
@@ -15,7 +15,11 @@
 from .export_import import GroupExportManager, GroupImportManager  # noqa: F401
 from .issues import GroupIssueManager  # noqa: F401
 from .labels import GroupLabelManager  # noqa: F401
-from .members import GroupMemberAllManager, GroupMemberManager  # noqa: F401
+from .members import (  # noqa: F401
+    GroupBillableMemberManager,
+    GroupMemberAllManager,
+    GroupMemberManager,
+)
 from .merge_requests import GroupMergeRequestManager  # noqa: F401
 from .milestones import GroupMilestoneManager  # noqa: F401
 from .notification_settings import GroupNotificationSettingsManager  # noqa: F401
@@ -38,6 +42,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
         ("accessrequests", "GroupAccessRequestManager"),
         ("audit_events", "GroupAuditEventManager"),
         ("badges", "GroupBadgeManager"),
+        ("billable_members", "GroupBillableMemberManager"),
         ("boards", "GroupBoardManager"),
         ("customattributes", "GroupCustomAttributeManager"),
         ("exports", "GroupExportManager"),
diff --git a/gitlab/v4/objects/members.py b/gitlab/v4/objects/members.py
--- a/gitlab/v4/objects/members.py
+++ b/gitlab/v4/objects/members.py
@@ -2,6 +2,8 @@
 from gitlab.base import RequiredOptional, RESTManager, RESTObject
 from gitlab.mixins import (
     CRUDMixin,
+    DeleteMixin,
+    ListMixin,
     MemberAllMixin,
     ObjectDeleteMixin,
     RetrieveMixin,
@@ -9,6 +11,10 @@
 )
 
 __all__ = [
+    "GroupBillableMember",
+    "GroupBillableMemberManager",
+    "GroupBillableMemberMembership",
+    "GroupBillableMemberMembershipManager",
     "GroupMember",
     "GroupMemberManager",
     "GroupMemberAllManager",
@@ -35,6 +41,28 @@ class GroupMemberManager(MemberAllMixin, CRUDMixin, RESTManager):
     _types = {"user_ids": types.ListAttribute}
 
 
+class GroupBillableMember(ObjectDeleteMixin, RESTObject):
+    _short_print_attr = "username"
+    _managers = (("memberships", "GroupBillableMemberMembershipManager"),)
+
+
+class GroupBillableMemberManager(ListMixin, DeleteMixin, RESTManager):
+    _path = "/groups/%(group_id)s/billable_members"
+    _obj_cls = GroupBillableMember
+    _from_parent_attrs = {"group_id": "id"}
+    _list_filters = ("search", "sort")
+
+
+class GroupBillableMemberMembership(RESTObject):
+    _id_attr = "user_id"
+
+
+class GroupBillableMemberMembershipManager(ListMixin, RESTManager):
+    _path = "/groups/%(group_id)s/billable_members/%(user_id)s/memberships"
+    _obj_cls = GroupBillableMemberMembership
+    _from_parent_attrs = {"group_id": "group_id", "user_id": "id"}
+
+
 class GroupMemberAllManager(RetrieveMixin, RESTManager):
     _path = "/groups/%(group_id)s/members/all"
     _obj_cls = GroupMember


Test patch:
diff --git a/gitlab/tests/objects/test_members.py b/gitlab/tests/objects/test_members.py
new file mode 100644
--- /dev/null
+++ b/gitlab/tests/objects/test_members.py
@@ -0,0 +1,58 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/members.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import GroupBillableMember
+
+billable_members_content = [
+    {
+        "id": 1,
+        "username": "raymond_smith",
+        "name": "Raymond Smith",
+        "state": "active",
+        "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+        "web_url": "http://192.168.1.8:3000/root",
+        "last_activity_on": "2021-01-27",
+        "membership_type": "group_member",
+        "removable": True,
+    }
+]
+
+
+@pytest.fixture
+def resp_list_billable_group_members():
+    with responses.RequestsMock() as rsps:
+        rsps.add(
+            method=responses.GET,
+            url="http://localhost/api/v4/groups/1/billable_members",
+            json=billable_members_content,
+            content_type="application/json",
+            status=200,
+        )
+        yield rsps
+
+
+@pytest.fixture
+def resp_delete_billable_group_member(no_content):
+    with responses.RequestsMock() as rsps:
+        rsps.add(
+            method=responses.DELETE,
+            url="http://localhost/api/v4/groups/1/billable_members/1",
+            json=no_content,
+            content_type="application/json",
+            status=204,
+        )
+        yield rsps
+
+
+def test_list_group_billable_members(group, resp_list_billable_group_members):
+    billable_members = group.billable_members.list()
+    assert isinstance(billable_members, list)
+    assert isinstance(billable_members[0], GroupBillableMember)
+    assert billable_members[0].removable is True
+
+
+def test_delete_group_billable_member(group, resp_delete_billable_group_member):
+    group.billable_members.delete(1)